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.
This commit is contained in:
2026-08-18 17:06:05 +02:00
parent 69bd17a296
commit 3741ce34ac
14 changed files with 566 additions and 47 deletions
+36
View File
@@ -0,0 +1,36 @@
---
import Base from '../../layouts/Base.astro';
import PageHero from '../../components/PageHero.astro';
import T from '../../components/content/T.astro';
import { t } from '../../lib/content';
export const prerender = false;
// Gli eventi del centro. Oggi ce n'è uno solo — l'inaugurazione del 19 settembre — e il
// programma della giornata non c'è ancora: il testo lo dichiara invece di lasciare una
// pagina vuota, ed è editabile dal pannello, così il cliente lo completa senza un deploy.
const wa = t('global.whatsapp');
const waLink = `https://wa.me/39${wa.replace(/\D/g, '')}`;
---
<Base title={t('eventi.meta.title')} description={t('eventi.meta.description')}>
<PageHero title={t('eventi.hero.title')} />
<section class="section">
<div class="container">
<article class="evento" data-reveal>
<T tag="eventi.inaugurazione.title" as="h2" />
<T tag="eventi.inaugurazione.body" as="p" />
<a class="btn" href={waLink} target="_blank" rel="noopener">{t('eventi.cta.label')}</a>
</article>
<p class="evento__torna"><a href="/promo-eventi">{t('eventi.torna.label')}</a></p>
</div>
</section>
</Base>
<style>
.evento { max-width: 760px; margin: 0 auto; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 20px; }
.evento :global(h2) { text-transform: uppercase; letter-spacing: .1em; margin: 0; }
.evento :global(p) { margin: 0; }
.evento__torna { text-align: center; margin-top: 46px; }
.evento__torna a { font-family: var(--font-heading); font-size: .72rem; letter-spacing: .18em; text-transform: uppercase; color: var(--c-heading); text-decoration: none; }
.evento__torna a:hover { color: var(--c-accent-dark); }
</style>
+56
View File
@@ -0,0 +1,56 @@
---
import Base from '../../layouts/Base.astro';
import PageHero from '../../components/PageHero.astro';
import T from '../../components/content/T.astro';
import { t } from '../../lib/content';
import { edizioneAttiva } from '../../data/promo-edizioni';
export const prerender = false;
// Chi entra da «Promo ed eventi» sceglie: le promozioni in corso oppure gli eventi del
// centro (chiesto dal cliente il 17/08/2026). Erano la stessa pagina, e chi cercava
// l'inaugurazione finiva in mezzo ai prezzi dei pacchetti.
//
// Il bottone delle promo dice se ce n'è davvero una in corso: mandare su una pagina che
// risponde «nessuna promozione» senza averlo detto prima è un giro a vuoto.
const ed = edizioneAttiva(new Date());
// La pagina cambia contenuto col passare del tempo (l'edizione promo scade da sola): una
// copia in cache la congelerebbe a com'era.
Astro.response.headers.set('Cache-Control', 'no-store');
---
<Base title={t('promo-eventi.meta.title')} description={t('promo-eventi.meta.description')}>
<PageHero title={t('promo-eventi.hero.title')} />
<section class="section">
<div class="container">
<div class="section-heading">
<T tag="promo-eventi.intro" as="p" />
</div>
<div class="scelta">
<a class="scelta__card" href="/promo-eventi/promo">
<T tag="promo-eventi.promo.label" as="h2" />
<T tag="promo-eventi.promo.desc" as="p" />
{!ed && <T tag="promo-eventi.promo.nessuna" as="p" class="scelta__nota" />}
<span class="btn">{t('promo-eventi.promo.cta')}</span>
</a>
<a class="scelta__card scelta__card--dark" href="/promo-eventi/eventi">
<T tag="promo-eventi.eventi.label" as="h2" />
<T tag="promo-eventi.eventi.desc" as="p" />
<span class="btn btn--light">{t('promo-eventi.eventi.cta')}</span>
</a>
</div>
</div>
</section>
</Base>
<style>
.scelta { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; }
.scelta__card { display: flex; flex-direction: column; align-items: flex-start; gap: 14px; padding: 48px 40px; background: #fff; box-shadow: 0 6px 24px rgba(0,0,0,.08); text-decoration: none; transition: transform .3s ease, box-shadow .3s ease; }
.scelta__card:hover { transform: translateY(-4px); box-shadow: 0 12px 32px rgba(0,0,0,.12); }
.scelta__card :global(h2) { text-transform: uppercase; letter-spacing: .12em; font-size: 1.4rem; margin: 0; }
.scelta__card :global(p) { font-size: .92rem; margin: 0; }
.scelta__card .btn { margin-top: auto; }
.scelta__card--dark { background: var(--c-dark); }
.scelta__card--dark :global(h2), .scelta__card--dark :global(p) { color: #efe9e3; }
.scelta__nota { font-family: var(--font-heading); font-size: .72rem; letter-spacing: .14em; text-transform: uppercase; color: var(--c-accent-dark); }
@media (max-width: 767px) { .scelta { grid-template-columns: 1fr; } .scelta__card { padding: 34px 26px; } }
</style>
@@ -1,10 +1,10 @@
---
import Base from '../layouts/Base.astro';
import T from '../components/content/T.astro';
import { t, resolveContent } from '../lib/content';
import { edizioneAttiva, tagEdizione } from '../data/promo-edizioni';
import { getSessionUser, SESSION_COOKIE } from '../lib/auth';
import { getDb } from '../lib/db';
import Base from '../../layouts/Base.astro';
import T from '../../components/content/T.astro';
import { t, resolveContent } from '../../lib/content';
import { edizioneAttiva, tagEdizione } from '../../data/promo-edizioni';
import { getSessionUser, SESSION_COOKIE } from '../../lib/auth';
import { getDb } from '../../lib/db';
// Landing promo/lancio integrata nel sito (header e footer del sito, non standalone).
// I bottoni "Acquista" portano al gestionale bsport: l'utente crea l'account e sceglie lì
// dentro il prodotto, poi paga via Stripe. Il link è uno solo per tutte le card, ed è voluto:
+17 -4
View File
@@ -8,18 +8,20 @@ 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, SERVIZI_CON_CALENDARIO } from '../../data/calendari';
import { calendariDaMostrare, PAGINE_CON_CALENDARIO } from '../../data/calendari';
import { t } from '../../lib/content';
export const prerender = false;
const service = getService(Astro.params.slug!);
if (!service) return new Response(null, { status: 404 });
// `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 = SERVIZI_CON_CALENDARIO.includes(slug) && calendariDaMostrare(new Date()).length > 0;
const others = services.filter((p) => p.slug !== service.slug && p.grid !== false);
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);
@@ -44,6 +46,15 @@ const tones = ['tan', 'taupe', 'dark'];
{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}>
@@ -119,6 +130,8 @@ const tones = ['tan', 'taupe', 'dark'];
.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; }
+22 -4
View File
@@ -5,6 +5,8 @@ import type { ImageMetadata } from 'astro';
import { services } from '../../data/services';
import T from '../../components/content/T.astro';
import TImg from '../../components/content/TImg.astro';
import CalendarioModal from '../../components/CalendarioModal.astro';
import { calendariDaMostrare, CARD_CON_CALENDARIO } from '../../data/calendari';
import { t } from '../../lib/content';
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;
@@ -13,7 +15,13 @@ const gridServices = services.filter((p) => p.grid !== false);
// costruzione: erano due espressioni diverse e sono divergute — su One to One e Performance
// Class il testo mandava a /training mentre la foto apriva la scheda del servizio, così la
// destinazione dipendeva da dove capitava il dito.
const hrefOf = (slug: string) => `/services/${slug}`;
// Alcune card non portano alla propria scheda ma a una vista di /training (One to One e
// Class, deciso dal cliente il 17/08/2026): la destinazione la dichiara il servizio.
const hrefOf = (p: (typeof services)[number]) => p.href ?? `/services/${p.slug}`;
// Il calendario delle class si apre anche da qui, dalla card Class: chi guarda i servizi
// vuole sapere quando si allena prima ancora di aprire la scheda. Compare solo se c'è
// davvero una griglia da mostrare, come nelle pagine delle class.
const conCalendario = calendariDaMostrare(new Date()).length > 0;
export const prerender = false;
---
<Base title={t('services.meta.title')} description={t('services.meta.description')}>
@@ -27,7 +35,7 @@ export const prerender = false;
{gridServices.map((p, i) => (
<article class="plist__card" data-reveal style={`--reveal-delay:${((i % 3) * 0.15).toFixed(2)}s`}>
{imgOf(p.slug) && (
<a class="plist__photo" href={hrefOf(p.slug)}>
<a class="plist__photo" href={hrefOf(p)}>
<TImg tag={`service.${p.slug}.image`} src={imgOf(p.slug)!} alt={t(`service.${p.slug}.title`)} widths={[400, 700]} sizes="(max-width:767px) 100vw, 33vw" />
</a>
)}
@@ -35,13 +43,17 @@ export const prerender = false;
<T tag={`service.${p.slug}.title`} as="h3" />
<T tag={`service.${p.slug}.subtitle`} as="p" class="plist__sub" />
<T tag={`service.${p.slug}.excerpt`} as="p" />
<a class="plist__more" href={hrefOf(p.slug)}>{t('services.card.more-label')}</a>
{conCalendario && CARD_CON_CALENDARIO.includes(p.slug) && (
<button type="button" class="plist__calendario" data-apri-calendario>{t('calendario.link-label')}</button>
)}
<a class="plist__more" href={hrefOf(p)}>{t('services.card.more-label')}</a>
</div>
</article>
))}
</div>
</div>
</section>
{conCalendario && <CalendarioModal />}
</Base>
<style>
@@ -53,9 +65,15 @@ export const prerender = false;
.plist__body { padding: 26px 24px 30px; display: flex; flex-direction: column; align-items: flex-start; flex: 1; }
.plist__card h3 { font-size: 1rem; text-transform: uppercase; letter-spacing: .1em; margin-bottom: .3em; }
.plist__sub { font-size: .78rem; text-transform: uppercase; letter-spacing: .12em; color: var(--c-accent-dark); }
.plist__body > p:not(.plist__sub) { font-size: .9rem; }
.plist__body > p:not(.plist__sub) { font-size: .9rem; white-space: pre-line; }
.plist__more { font-family: var(--font-heading); font-size: .72rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; text-decoration: none; color: var(--c-heading); margin-top: auto; }
.plist__more:hover { color: var(--c-accent-dark); }
/* Il calendario sta sopra "Per saperne di più" (posizione chiesta dal cliente) e apre un
popup, quindi è un bottone: qui gli si toglie l'aspetto da bottone perché nella card
stia accanto all'altra voce senza rubarle l'occhio. */
.plist__calendario { background: none; border: 0; padding: 0; margin-top: auto; cursor: pointer; text-align: left; font-family: var(--font-heading); font-size: .72rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; color: var(--c-accent-dark); }
.plist__calendario:hover { color: var(--c-heading); }
.plist__calendario + .plist__more { margin-top: 10px; }
@media (max-width: 991px) { .plist { grid-template-columns: 1fr 1fr; } }
@media (max-width: 600px) { .plist { grid-template-columns: 1fr; } }
</style>
@@ -1,28 +1,37 @@
---
import Base from '../layouts/Base.astro';
import PageHero from '../components/PageHero.astro';
import TrainingCards from '../components/home/TrainingCards.astro';
import Base from '../../layouts/Base.astro';
import PageHero from '../../components/PageHero.astro';
import TrainingCards from '../../components/home/TrainingCards.astro';
import { Image } from 'astro:assets';
import { trainings } from '../data/trainings';
import T from '../components/content/T.astro';
import { t } from '../lib/content';
const images = import.meta.glob<{ default: ImageMetadata }>('../assets/img/training-*.{jpg,png}', { eager: true });
import type { ImageMetadata } from 'astro';
import { VISTE_TRAINING, trainingDaMostrare } from '../../data/trainings';
import T from '../../components/content/T.astro';
import { t } from '../../lib/content';
const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/training-*.{jpg,png}', { eager: true });
const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1]?.default;
export const prerender = false;
// `/training` mostra tutto; `/training/one-to-one` e `/training/class` il solo sottoinsieme
// da cui si arriva cliccando la card corrispondente in /services. Un indirizzo inventato è
// 404 e non l'elenco intero: un filtro che non c'è deve vedersi, non passare per buono.
const vista = Astro.params.vista;
const daMostrare = trainingDaMostrare(vista);
if (!daMostrare) return new Response(null, { status: 404 });
const filtrata = Boolean(vista);
---
<Base title={t('training.meta.title')} description={t('training.meta.description')}>
<PageHero title={t('training.hero.title')} />
<section class="section" style="padding-bottom:0">
<div class="section-heading">
<T tag="training.intro.subtitle" as="p" />
{filtrata ? <p>{VISTE_TRAINING[vista!].titolo}</p> : <T tag="training.intro.subtitle" as="p" />}
</div>
</section>
<TrainingCards />
<TrainingCards items={daMostrare} />
<section class="section">
<div class="container">
{trainings.map((tr, i) => (
{daMostrare.map((tr, i) => (
<div class:list={['tblock', { 'tblock--reverse': i % 2 === 1 }]} id={tr.id}>
{imgOf(tr.image) && <Image src={imgOf(tr.image)!} alt={t(`training.${tr.id}.title`)} widths={[500, 900]} sizes="(max-width:767px) 100vw, 45vw" data-reveal={i % 2 === 1 ? 'right' : 'left'} />}
<div data-reveal={i % 2 === 1 ? 'left' : 'right'}>
@@ -32,6 +41,11 @@ export const prerender = false;
</div>
</div>
))}
{/* Da una vista filtrata si esce: senza questo link chi è entrato da una card di
/services non ha modo di vedere gli altri training se non dal menu. */}
{filtrata && (
<p class="tblock__tutti"><a class="btn btn--light" href="/training">{t('training.tutti.cta-label')}</a></p>
)}
</div>
</section>
</Base>
@@ -40,5 +54,6 @@ export const prerender = false;
.tblock { display: grid; grid-template-columns: 1fr 1fr; gap: 60px; align-items: center; padding-block: 40px; scroll-margin-top: calc(var(--header-h) + 20px); }
.tblock--reverse img { order: 2; }
.tblock h2 { letter-spacing: .1em; }
.tblock__tutti { text-align: center; margin-top: 20px; }
@media (max-width: 767px) { .tblock { grid-template-columns: 1fr; gap: 24px; } .tblock--reverse img { order: 0; } }
</style>