Deploy stack su Proxmox / deploy (push) Failing after 3s
Monorepo npm con apps/public (Angular SSR), apps/private (Angular SPA), apps/api (Fastify), packages/shared e packages/styles; Medusa 2.21 in medusa/; infrastruttura Docker (Postgres PostGIS+pgvector, Redis, Caddy, backup) e workflow Gitea Actions per il deploy via SSH a ogni push su master. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018jQyT6dR9czzGwqb7FkG6u
152 lines
4.8 KiB
TypeScript
152 lines
4.8 KiB
TypeScript
import { NgOptimizedImage } from '@angular/common'
|
|
import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core'
|
|
import { RouterLink } from '@angular/router'
|
|
import { AREAS, findArea } from '../../content/areas'
|
|
import { PROJECTS } from '../../content/projects'
|
|
import { SeoService } from '../core/seo.service'
|
|
import { PageHeader } from '../shared/page-header'
|
|
import { PlaceholderBadge } from '../shared/placeholder-badge'
|
|
import { QuoteCta } from '../shared/quote-cta'
|
|
|
|
/**
|
|
* /realizzazioni: galleria da src/content/projects.ts, filtrabile per area con ?area=<slug>
|
|
* (link veri, quindi il filtro funziona anche senza JavaScript e lato server).
|
|
*/
|
|
@Component({
|
|
selector: 'app-projects-page',
|
|
imports: [NgOptimizedImage, RouterLink, PageHeader, PlaceholderBadge, QuoteCta],
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
template: `
|
|
<app-page-header heading="Realizzazioni" lead="Alcuni degli impianti che abbiamo progettato e installato." [crumbs]="[{ label: 'Realizzazioni' }]">
|
|
<app-placeholder-badge badge [show]="hasPlaceholders" />
|
|
</app-page-header>
|
|
<div class="container body">
|
|
<nav aria-label="Filtra per area">
|
|
<ul class="filters">
|
|
<li><a routerLink="/realizzazioni" [attr.aria-current]="!selected() ? 'true' : null">Tutte</a></li>
|
|
@for (a of areas; track a.slug) {
|
|
<li>
|
|
<a routerLink="/realizzazioni" [queryParams]="{ area: a.slug }" [attr.aria-current]="selected() === a.slug ? 'true' : null">{{ a.label }}</a>
|
|
</li>
|
|
}
|
|
</ul>
|
|
</nav>
|
|
|
|
<ul class="gallery">
|
|
@for (p of projects(); track p.id; let i = $index) {
|
|
<li class="card">
|
|
<img [ngSrc]="p.image.src" [width]="p.image.width" [height]="p.image.height" [alt]="p.image.alt" [priority]="i < 2" />
|
|
<div class="text">
|
|
<p class="meta">{{ areaLabel(p.area) }} · {{ p.location }} · {{ p.year }}</p>
|
|
<h2>{{ p.title }}</h2>
|
|
<p>{{ p.summary }}</p>
|
|
</div>
|
|
</li>
|
|
} @empty {
|
|
<li class="empty">Nessuna realizzazione in quest'area per ora.</li>
|
|
}
|
|
</ul>
|
|
|
|
<app-quote-cta heading="Vuoi un risultato così?" />
|
|
</div>
|
|
`,
|
|
styles: `
|
|
@use 'mixins' as mq;
|
|
|
|
.body {
|
|
padding-block: var(--space-5) var(--space-4);
|
|
}
|
|
.filters {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--space-2);
|
|
margin: 0 0 var(--space-5);
|
|
padding: 0;
|
|
list-style: none;
|
|
|
|
a {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
min-height: 44px;
|
|
padding-inline: var(--space-4);
|
|
border: 1px solid var(--color-line);
|
|
border-radius: var(--radius-pill);
|
|
color: var(--color-ink);
|
|
text-decoration: none;
|
|
|
|
&[aria-current='true'] {
|
|
border-color: var(--color-primary);
|
|
background: var(--color-primary);
|
|
color: var(--color-on-primary);
|
|
}
|
|
}
|
|
}
|
|
.gallery {
|
|
display: grid;
|
|
gap: var(--space-5);
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
|
|
@include mq.up(md) {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
@include mq.up(lg) {
|
|
grid-template-columns: repeat(3, 1fr);
|
|
}
|
|
}
|
|
.card {
|
|
overflow: hidden;
|
|
border: 1px solid var(--color-line);
|
|
border-radius: var(--radius-lg);
|
|
|
|
img {
|
|
width: 100%;
|
|
height: auto;
|
|
}
|
|
}
|
|
.text {
|
|
padding: var(--space-4) var(--space-5) var(--space-5);
|
|
|
|
h2 {
|
|
font-size: var(--font-size-lg);
|
|
margin-bottom: var(--space-2);
|
|
}
|
|
p:last-child {
|
|
margin: 0;
|
|
color: var(--color-ink-muted);
|
|
}
|
|
}
|
|
.meta {
|
|
margin-bottom: var(--space-2);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: var(--font-weight-semibold);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: var(--color-secondary);
|
|
}
|
|
`,
|
|
})
|
|
export default class ProjectsPage {
|
|
/** Query param ?area= (withComponentInputBinding). */
|
|
readonly area = input<string>()
|
|
|
|
protected readonly areas = AREAS
|
|
protected readonly hasPlaceholders = PROJECTS.some((p) => p.placeholder)
|
|
protected readonly selected = computed(() => (this.area() && findArea(this.area()!) ? this.area() : undefined))
|
|
protected readonly projects = computed(() => {
|
|
const area = this.selected()
|
|
return area ? PROJECTS.filter((p) => p.area === area) : PROJECTS
|
|
})
|
|
protected readonly areaLabel = (slug: string) => findArea(slug)?.label ?? slug
|
|
|
|
constructor() {
|
|
// Una sola pagina canonica: i filtri ?area= puntano tutti a /realizzazioni
|
|
inject(SeoService).setPage({
|
|
title: 'Realizzazioni',
|
|
description: 'Impianti fotovoltaici, pompe di calore, sistemi di trattamento acqua e ricarica realizzati da ITI TECHNOLOGY.',
|
|
path: '/realizzazioni',
|
|
})
|
|
}
|
|
}
|