Files
InsanityLab-web/src/pages/services/[slug].astro
T
Adriano 3741ce34ac Modifiche chieste dal cliente il 17 agosto
Il documento MODIFICHE_PER_SITO.pdf porta cinque cose.

1. "Small Group" si chiama "Personalized Training Group". Il nome vive anche
   nel database dei contenuti, dove `resolveContent` legge prima della copia
   nel codice: `scripts/testi-17-agosto.mjs` aggiorna le 19 righe rimaste,
   una volta sola, correggendo anche gli articoli ("Lo Small Group" -> "Il
   Personalized Training Group"). Lo slug della scheda resta `small-group`:
   cambiarlo romperebbe i link gia' girati su WhatsApp.

2. Dalla pagina Servizi le card instradano dove serve. "One to One" apre la
   vista Training con One to One, Personalized Training Group e Rehab;
   "Class" quella con Holistic e Reformer. Le viste sono `/training/<vista>`;
   una vista inventata e' 404, non l'elenco intero, altrimenti un indirizzo
   rotto continuerebbe a sembrare buono.

3. La Performance Class non si eroga piu': la scheda che la spiegava
   risponde 404, la card "Class" resta in griglia. Con la scheda e' caduta la
   tabella prezzi, che era solo sua.

4. Il Check-up racconta Base e Avanzato: trafiletto nuovo nella card e
   sezione "Check-up Avanzato" nella scheda, con la Wellness Tower e i cinque
   punti. Il calendario delle class si apre ora anche dalla card Class in
   griglia e dalla scheda Reformer, oltre che da Holistic.

5. "Promo ed eventi" diventa una pagina di scelta: le promozioni restano su
   /promo-eventi/promo, l'inaugurazione del 19 settembre sta su
   /promo-eventi/eventi. Il testo dice che il programma della giornata manca
   ancora, ed e' editabile dal pannello.

Corretti due link del footer che portavano a schede inesistenti
(`/services/personal-training` non e' mai stato uno slug).

181 test verdi (157 + 24 nuovi), build ok, `astro check` invariato.
2026-08-18 17:06:05 +02:00

163 lines
9.3 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import Base from '../../layouts/Base.astro';
import PageHero from '../../components/PageHero.astro';
import type { ImageMetadata } from 'astro';
import { services, getService } from '../../data/services';
import { performanceClassPricing } from '../../data/pricing';
import { serviceIcons } from '../../data/service-icons';
import T from '../../components/content/T.astro';
import TImg from '../../components/content/TImg.astro';
import CalendarioModal from '../../components/CalendarioModal.astro';
import { calendariDaMostrare, PAGINE_CON_CALENDARIO } from '../../data/calendari';
import { t } from '../../lib/content';
export const prerender = false;
const service = getService(Astro.params.slug!);
// `page: false` = servizio senza scheda (oggi la Performance Class, che non si eroga più):
// la voce esiste ancora nei dati per la card in griglia, ma la pagina non c'è.
if (!service || service.page === false) return new Response(null, { status: 404 });
const slug = service.slug;
// Il calendario della sala si apre dalle pagine delle class. La voce compare solo se c'è
// davvero una griglia da mostrare (in vigore o prossima): un link che apre un popup vuoto è
// peggio di nessun link.
const conCalendario = PAGINE_CON_CALENDARIO.includes(slug) && calendariDaMostrare(new Date()).length > 0;
const others = services.filter((p) => p.slug !== service.slug && p.grid !== false && p.page !== false);
const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/program-*.jpg', { eager: true });
const imgOf = (slug: string) => Object.entries(images).find(([p]) => p.includes(`program-${slug}.`))?.[1]?.default;
const heroImg = imgOf(service.slug);
// Categorie come da Figma (Performance class.pdf), non quelle del blog → tag service.side.category.1-4
const categoryIndexes = [1, 2, 3, 4];
// Ordine card pricing come da Figma: tan, taupe, dark
const tones = ['tan', 'taupe', 'dark'];
---
<Base title={t(`service.${slug}.title`)} description={t(`service.${slug}.excerpt`)}>
<PageHero title={(service.heroTitle ?? t(`service.${slug}.title`)).toUpperCase()} bgImage={heroImg} />
<section class="section">
<div class="container prog">
<div>
<h2>{t(`service.${slug}.title`).toUpperCase()}</h2>
<T tag={`service.${slug}.long-description`} as="p" class="prog__desc" />
{service.features.length > 0 && (
<ul class="prog__features">
{service.features.map((_, i) => <T tag={`service.${slug}.feature.${i + 1}`} as="li" />)}
</ul>
)}
{heroImg && (
<TImg tag={`service.${slug}.image`} src={heroImg} alt={t(`service.${slug}.title`)} class="prog__img" widths={[600, 1000]} sizes="(max-width:991px) 100vw, 60vw" data-reveal="fade" />
)}
{service.extra && (
<div class="prog__extra">
<T tag={`service.${slug}.extra.title`} as="h2" />
<T tag={`service.${slug}.extra.body`} as="p" class="prog__desc" />
<ul class="prog__features">
{service.extra.items.map((_, i) => <T tag={`service.${slug}.extra.item.${i + 1}`} as="li" />)}
</ul>
</div>
)}
<div class="prog__faq">
{service.faq.map((_, i) => (
<details open={i === 0}>
<summary><T tag={`service.${slug}.faq.${i + 1}.q`} /></summary>
<T tag={`service.${slug}.faq.${i + 1}.a`} as="p" />
</details>
))}
</div>
</div>
<aside class="prog__side">
<T tag="service.side.categories-label" as="h3" />
{categoryIndexes.map((n) => <T tag={`service.side.category.${n}`} as="p" class="prog__cat" />)}
<T tag="service.side.others-label" as="h3" />
{others.map((o) => (
<a class="prog__other" href={`/services/${o.slug}`}>
<span class="prog__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" width="26" height="26" aria-hidden="true"><path d={serviceIcons[o.slug]} /></svg>
</span>
<T tag={`service.${o.slug}.title`} />
</a>
))}
{conCalendario && (
<button type="button" class="prog__other prog__other--calendario" data-apri-calendario>
<span class="prog__icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" width="26" height="26" aria-hidden="true">
<path d="M4 6h16v14H4zM4 10h16M8 3v4M16 3v4M8 14h3M8 17h3M14 14h2" />
</svg>
</span>
{t('calendario.link-label')}
</button>
)}
</aside>
</div>
</section>
{service.hasPricing && (
<section class="section section--alt">
<div class="container">
<div class="section-heading">
<T tag="pricing.heading" as="h2" />
<T tag="pricing.intro" as="p" />
</div>
{performanceClassPricing.groups.map((g, gi) => (
<details class="price__group" open={g.open}>
<summary>{g.open ? '+' : ''} <T tag={`pricing.group.${gi + 1}.label`} /></summary>
{g.plans.length === 0 ? (
<T tag="service.pricing.empty-note" as="p" class="price__empty" />
) : (
<div class="price__row">
{g.plans.map((p, i) => (
<div class:list={['price__card', `price__card--${tones[i % 3]}`]}>
<p class="price__amount">{p.monthly}€</p>
<T tag="service.pricing.per" as="p" class="price__per" />
<p class="price__entries">{p.entries} {p.entries === 1 ? t('service.pricing.entries-singular') : t('service.pricing.entries-plural')}</p>
<T tag={`pricing.group.${gi + 1}.plan.${i + 1}.note`} as="p" />
<T tag={`pricing.group.${gi + 1}.plan.${i + 1}.installments`} as="p" />
<T tag={`pricing.group.${gi + 1}.plan.${i + 1}.best`} as="p" class="price__best" />
<a class="btn btn--light" href="/contact">{t('service.pricing.buy-cta')}</a>
</div>
))}
</div>
)}
</details>
))}
</div>
</section>
)}
{conCalendario && <CalendarioModal />}
</Base>
<style>
.prog { display: grid; grid-template-columns: 2.2fr 1fr; gap: 60px; }
.prog :global(.prog__desc) { white-space: pre-line; }
.prog__features { padding-left: 20px; display: grid; gap: 8px; margin-bottom: 30px; }
.prog__extra { margin-bottom: 10px; }
.prog__extra h2 { letter-spacing: .1em; margin-bottom: .5em; }
.prog__features li::marker { color: var(--c-accent); }
.prog__img { width: 100%; height: auto; margin-bottom: 30px; }
.prog__faq details { border-top: 1px solid #e4ded6; padding-block: 14px; }
.prog__faq summary { cursor: pointer; font-family: var(--font-heading); font-size: .85rem; letter-spacing: .08em; color: var(--c-heading); }
.prog__side h3 { font-size: .85rem; text-transform: uppercase; letter-spacing: .16em; margin-top: 30px; }
.prog__cat { font-family: var(--font-heading); font-size: .72rem; letter-spacing: .18em; text-transform: uppercase; color: var(--c-text); margin-bottom: 10px; }
.prog__other { display: flex; align-items: center; gap: 14px; text-decoration: none; color: var(--c-text); margin-bottom: 14px; font-family: var(--font-heading); font-size: .78rem; letter-spacing: .14em; text-transform: uppercase; }
.prog__other:hover { color: var(--c-accent-dark); }
/* La voce del calendario apre un popup, quindi è un bottone e non un link: qui gli si
toglie l'aspetto da bottone perché nella colonna sia indistinguibile dalle altre voci. */
.prog__other--calendario { background: none; border: 0; padding: 0; cursor: pointer; width: 100%; text-align: left; }
.prog__icon { display: inline-flex; align-items: center; justify-content: center; width: 48px; height: 48px; background: var(--c-accent); color: #fff; flex-shrink: 0; }
.price__group { border-top: 1px solid #ddd5ca; padding-block: 18px; }
.price__group summary { cursor: pointer; font-family: var(--font-heading); letter-spacing: .12em; text-transform: uppercase; font-size: .85rem; color: var(--c-heading); }
.price__row { display: grid; grid-template-columns: repeat(3, 1fr); gap: 30px; margin-top: 30px; }
/* Card colorate come nel Figma: tan / taupe / dark, testo bianco, prezzo grande */
.price__card { padding: 44px 28px; text-align: center; color: #fff; }
.price__card--tan { background: var(--c-accent); }
.price__card--taupe { background: #6b5d5b; }
.price__card--dark { background: var(--c-dark); }
.price__card .price__amount { font-family: var(--font-heading); font-size: clamp(2.6rem, 4.7vw, 4rem); font-weight: 500; color: #fff; margin: 0; line-height: 1.1; }
.price__card .price__per { font-family: var(--font-heading); text-transform: uppercase; letter-spacing: .18em; font-size: .68rem; opacity: .85; margin-bottom: 22px; }
.price__card .price__entries { font-family: var(--font-heading); text-transform: uppercase; letter-spacing: .1em; font-size: .82rem; border-top: 1px solid rgba(255,255,255,.25); padding-top: 18px; }
.price__card p { margin-bottom: 6px; font-size: .82rem; }
.price__card .price__best { font-weight: 700; text-transform: uppercase; letter-spacing: .08em; font-size: .8rem; }
.price__card .btn { margin-top: 18px; }
@media (max-width: 991px) { .prog { grid-template-columns: 1fr; } .price__row { grid-template-columns: 1fr; } }
</style>