Piattaforma ITI TECHNOLOGY: monorepo, Medusa, API, sito, area riservata e deploy
Deploy stack su Proxmox / deploy (push) Failing after 3s
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
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
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',
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user