Files
InsanityLab-web/.superpowers/sdd/review-b84463c..31354c4.diff
T
Adriano b913a2bf90 registri dei piani in git (AI-OS#309)
Refs gitea:Adriano/AI-OS#310

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 10:36:26 +02:00

287 lines
15 KiB
Diff
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.
# Review package: b84463c..31354c4
## Commits
31354c4 feat: tag contenuti programmi e pricing
## Files changed
src/data/content-seed.ts | 49 +++++++++++++++++++++++++++++++++++
src/pages/programs/[slug].astro | 57 ++++++++++++++++++++++-------------------
src/pages/programs/index.astro | 22 ++++++++--------
3 files changed, 91 insertions(+), 37 deletions(-)
## Diff
diff --git a/src/data/content-seed.ts b/src/data/content-seed.ts
index 936b7a8..d4db795 100644
--- a/src/data/content-seed.ts
+++ b/src/data/content-seed.ts
@@ -1,17 +1,19 @@
import type { StyleGroup } from '../lib/style-presets';
import { site } from './site';
import { trainings } from './trainings';
import { team } from './team';
import { methodIntro, methodExtraNote, methodSteps } from './method';
import { partners } from './partners';
import { disciplines, days, slots } from './agenda';
+import { programs } from './programs';
+import { performanceClassPricing } from './pricing';
export type ContentType = 'text' | 'html' | 'image';
export interface SeedEntry { tag: string; type: ContentType; value: string; styleOptions?: StyleGroup[] }
const T = (tag: string, value: string, styleOptions: StyleGroup[] = ['size', 'color', 'weight', 'style']): SeedEntry =>
({ tag, type: 'text', value, styleOptions });
const H = (tag: string, value: string, styleOptions: StyleGroup[] = ['size', 'color']): SeedEntry =>
({ tag, type: 'html', value, styleOptions });
const I = (tag: string): SeedEntry => ({ tag, type: 'image', value: '' });
@@ -214,14 +216,61 @@ export const contentSeed: SeedEntry[] = [
T('about.storylab.title', 'Storylab'),
T('about.storylab.body', 'Scopri la nostra storia: come nasce InsanityLab, perché abbiamo deciso di creare qualcosa di diverso e chi sono i visionari che hanno trasformato questa idea in un nuovo modo di vivere benessere, movimento e salute.'),
T('about.timeline.label', 'INSANITYLAB 2021-2024', []),
T('about.timeline.open-text', "Il percorso di crescita di InsanityLab si è sviluppato attraverso tappe precise e coerenti con la visione iniziale. Nato nel 2021 con un piccolo team e grandi ambizioni, il progetto ha conosciuto una forte evoluzione nel 2024, anno in cui il servizio si è ampliato attraverso nuovi format come gli small group e una sempre maggiore specializzazione nella profilazione fisica del cliente, grazie a sistemi avanzati di valutazione e monitoraggio. Parallelamente, anche l'area dedicata alla riabilitazione e all'esercizio preventivo si è consolidata, ponendo le basi per un importante salto di qualità."),
T('about.timeline.stage1.label', 'INSANITYLAB 2025', []),
T('about.timeline.stage1.text', 'Contenuto in arrivo: il cliente fornirà il testo di questa tappa.'),
T('about.timeline.stage2.label', 'INSANITYLAB 2026', []),
T('about.timeline.stage2.text', 'Contenuto in arrivo: il cliente fornirà il testo di questa tappa.'),
T('about.timeline.stage3.label', 'INSANITYLAB OGGI', []),
T('about.timeline.stage3.text', 'Contenuto in arrivo: il cliente fornirà il testo di questa tappa.'),
+ // --- Programs: lista (/programs) ---
+ T('programs.meta.title', 'Programmi', []),
+ T('programs.meta.description', 'I programmi InsanityLab: Personal Training, Performance Class, Coaching, Pilates Flow, Rehab e Nutrizione.', []),
+ T('programs.hero.title', 'Programmi', []),
+ T('programs.intro.title', 'Programmi'),
+ T('programs.intro.body', "Questi sono i programmi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi un'esperienza di allenamento completa e mirata."),
+ T('programs.card.more-label', 'Per saperne di più →', []),
+
+ // --- Programs: dettaglio (data-driven da programs.ts) ---
+ ...programs.flatMap((p) => [
+ T(`program.${p.slug}.title`, p.title),
+ T(`program.${p.slug}.subtitle`, p.subtitle),
+ T(`program.${p.slug}.excerpt`, p.excerpt),
+ T(`program.${p.slug}.long-description`, p.longDescription),
+ ...p.features.map((f, i) => T(`program.${p.slug}.feature.${i + 1}`, f)),
+ ...p.faq.flatMap((item, i) => [
+ T(`program.${p.slug}.faq.${i + 1}.q`, item.q),
+ T(`program.${p.slug}.faq.${i + 1}.a`, item.a),
+ ]),
+ I(`program.${p.slug}.image`),
+ ]),
+
+ // --- Programs: dettaglio, template ---
+ T('program.side.categories-label', 'Categorie'),
+ T('program.side.category.1', 'Fitness', []),
+ T('program.side.category.2', 'Salute', []),
+ T('program.side.category.3', 'Stile di vita', []),
+ T('program.side.category.4', 'Nutrizione', []),
+ T('program.side.others-label', 'Altri programmi'),
+ H('program.pricing.empty-note', 'Contattaci per i dettagli di questo programma. <a href="/contact">Scrivici →</a>'),
+ T('program.pricing.per', 'al mese, semestrale', []),
+ T('program.pricing.entries-singular', 'ingresso settimanale', []),
+ T('program.pricing.entries-plural', 'ingressi settimanali', []),
+ T('program.pricing.buy-cta', 'Acquista', []),
+
+ // --- Pricing performance class (data-driven da pricing.ts; monthly/entries restano nel data file) ---
+ T('pricing.heading', performanceClassPricing.heading),
+ T('pricing.intro', performanceClassPricing.intro),
+ ...performanceClassPricing.groups.flatMap((g, gi) => [
+ T(`pricing.group.${gi + 1}.label`, g.label),
+ ...g.plans.flatMap((plan, pi) => [
+ T(`pricing.group.${gi + 1}.plan.${pi + 1}.note`, plan.note, []),
+ T(`pricing.group.${gi + 1}.plan.${pi + 1}.installments`, plan.installments, []),
+ T(`pricing.group.${gi + 1}.plan.${pi + 1}.best`, plan.best, []),
+ ]),
+ ]),
+
// I task di estrazione aggiungono qui le entry di header/footer/pagine/data file.
];
export const seedByTag = new Map(contentSeed.map((e) => [e.tag, e]));
diff --git a/src/pages/programs/[slug].astro b/src/pages/programs/[slug].astro
index 260fcc4..ae079db 100644
--- a/src/pages/programs/[slug].astro
+++ b/src/pages/programs/[slug].astro
@@ -1,95 +1,98 @@
---
import Base from '../../layouts/Base.astro';
import PageHero from '../../components/PageHero.astro';
-import { Image } from 'astro:assets';
import type { ImageMetadata } from 'astro';
import { programs } from '../../data/programs';
import { performanceClassPricing } from '../../data/pricing';
import { programIcons } from '../../data/program-icons';
+import T from '../../components/content/T.astro';
+import TImg from '../../components/content/TImg.astro';
+import { t } from '../../lib/content';
export const prerender = true;
export function getStaticPaths() {
return programs.map((p) => ({ params: { slug: p.slug }, props: { program: p } }));
}
const { program } = Astro.props;
+const slug = program.slug;
const others = programs.filter((p) => p.slug !== program.slug);
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(program.slug);
-// Categorie come da Figma (Performance class.pdf), non quelle del blog
-const categories = ['Fitness', 'Salute', 'Stile di vita', 'Nutrizione'];
+// Categorie come da Figma (Performance class.pdf), non quelle del blog → tag program.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={program.title} description={program.excerpt}>
- <PageHero title={program.title.toUpperCase()} bgImage={heroImg} />
+<Base title={t(`program.${slug}.title`)} description={t(`program.${slug}.excerpt`)}>
+ <PageHero title={t(`program.${slug}.title`).toUpperCase()} bgImage={heroImg} />
<section class="section">
<div class="container prog">
<div>
- <h2>{program.title.toUpperCase()}</h2>
- <p>{program.longDescription}</p>
+ <h2>{t(`program.${slug}.title`).toUpperCase()}</h2>
+ <T tag={`program.${slug}.long-description`} as="p" />
<ul class="prog__features">
- {program.features.map((f) => <li>{f}</li>)}
+ {program.features.map((_, i) => <T tag={`program.${slug}.feature.${i + 1}`} as="li" />)}
</ul>
{heroImg && (
- <Image src={heroImg} alt={program.title} class="prog__img" widths={[600, 1000]} sizes="(max-width:991px) 100vw, 60vw" data-reveal="fade" />
+ <TImg tag={`program.${slug}.image`} src={heroImg} alt={t(`program.${slug}.title`)} class="prog__img" widths={[600, 1000]} sizes="(max-width:991px) 100vw, 60vw" data-reveal="fade" />
)}
<div class="prog__faq">
- {program.faq.map((item, i) => (
+ {program.faq.map((_, i) => (
<details open={i === 0}>
- <summary>{item.q}</summary>
- <p>{item.a}</p>
+ <summary><T tag={`program.${slug}.faq.${i + 1}.q`} /></summary>
+ <T tag={`program.${slug}.faq.${i + 1}.a`} as="p" />
</details>
))}
</div>
</div>
<aside class="prog__side">
- <h3>Categorie</h3>
- {categories.map((c) => <p class="prog__cat">{c}</p>)}
- <h3>Altri programmi</h3>
+ <T tag="program.side.categories-label" as="h3" />
+ {categoryIndexes.map((n) => <T tag={`program.side.category.${n}`} as="p" class="prog__cat" />)}
+ <T tag="program.side.others-label" as="h3" />
{others.map((o) => (
<a class="prog__other" href={`/programs/${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={programIcons[o.slug]} /></svg>
</span>
- <span>{o.title}</span>
+ <T tag={`program.${o.slug}.title`} />
</a>
))}
</aside>
</div>
</section>
{program.hasPricing && (
<section class="section section--alt">
<div class="container">
<div class="section-heading">
- <h2>{performanceClassPricing.heading}</h2>
- <p>{performanceClassPricing.intro}</p>
+ <T tag="pricing.heading" as="h2" />
+ <T tag="pricing.intro" as="p" />
</div>
- {performanceClassPricing.groups.map((g) => (
+ {performanceClassPricing.groups.map((g, gi) => (
<details class="price__group" open={g.open}>
- <summary>{g.open ? '+' : '–'} {g.label}</summary>
+ <summary>{g.open ? '+' : '–'} <T tag={`pricing.group.${gi + 1}.label`} /></summary>
{g.plans.length === 0 ? (
- <p class="price__empty">Contattaci per i dettagli di questo programma. <a href="/contact">Scrivici →</a></p>
+ <T tag="program.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>
- <p class="price__per">al mese, semestrale</p>
- <p class="price__entries">{p.entries} {p.entries === 1 ? 'ingresso settimanale' : 'ingressi settimanali'}</p>
- <p>{p.note}</p>
- <p>{p.installments}</p>
- <p class="price__best">{p.best}</p>
- <a class="btn btn--light" href="/contact">Acquista</a>
+ <T tag="program.pricing.per" as="p" class="price__per" />
+ <p class="price__entries">{p.entries} {p.entries === 1 ? t('program.pricing.entries-singular') : t('program.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('program.pricing.buy-cta')}</a>
</div>
))}
</div>
)}
</details>
))}
</div>
</section>
)}
</Base>
diff --git a/src/pages/programs/index.astro b/src/pages/programs/index.astro
index 90f0530..9fb8554 100644
--- a/src/pages/programs/index.astro
+++ b/src/pages/programs/index.astro
@@ -1,41 +1,43 @@
---
import Base from '../../layouts/Base.astro';
import PageHero from '../../components/PageHero.astro';
-import { Image } from 'astro:assets';
import type { ImageMetadata } from 'astro';
import { programs } from '../../data/programs';
+import T from '../../components/content/T.astro';
+import TImg from '../../components/content/TImg.astro';
+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;
export const prerender = true;
---
-<Base title="Programmi" description="I programmi InsanityLab: Personal Training, Performance Class, Coaching, Pilates Flow, Rehab e Nutrizione.">
- <PageHero title="Programmi" />
+<Base title={t('programs.meta.title')} description={t('programs.meta.description')}>
+ <PageHero title={t('programs.hero.title')} />
<section class="section">
<div class="container">
<div class="section-heading">
- <h2>Programmi</h2>
- <p>Questi sono i programmi delle classi di Insanity Lab selezionati per voi. Abbiamo scelto i migliori percorsi per offrirvi un'esperienza di allenamento completa e mirata.</p>
+ <T tag="programs.intro.title" as="h2" />
+ <T tag="programs.intro.body" as="p" />
</div>
<div class="plist">
{programs.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={`/programs/${p.slug}`}>
- <Image src={imgOf(p.slug)!} alt={p.title} widths={[400, 700]} sizes="(max-width:767px) 100vw, 33vw" />
+ <TImg tag={`program.${p.slug}.image`} src={imgOf(p.slug)!} alt={t(`program.${p.slug}.title`)} widths={[400, 700]} sizes="(max-width:767px) 100vw, 33vw" />
</a>
)}
<div class="plist__body">
- <h3>{p.title}</h3>
- <p class="plist__sub">{p.subtitle}</p>
- <p>{p.excerpt}</p>
- <a class="plist__more" href={`/programs/${p.slug}`}>Per saperne di più →</a>
+ <T tag={`program.${p.slug}.title`} as="h3" />
+ <T tag={`program.${p.slug}.subtitle`} as="p" class="plist__sub" />
+ <T tag={`program.${p.slug}.excerpt`} as="p" />
+ <a class="plist__more" href={`/programs/${p.slug}`}>{t('programs.card.more-label')}</a>
</div>
</article>
))}
</div>
</div>
</section>
</Base>
<style>
.plist { display: grid; grid-template-columns: repeat(3, 1fr); gap: 40px 30px; }