Files
iti-technology/apps/public/src/app/pages/projects.ts
T
Lorenzo SanesiandClaude Opus 5.5 b83ce039fb
Deploy stack su Proxmox / deploy (push) Failing after 3s
Piattaforma ITI TECHNOLOGY: monorepo, Medusa, API, sito, area riservata e deploy
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
2026-10-20 09:30:48 +02:00

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',
})
}
}