Files
InsanityLab-web/.superpowers/sdd/review-3f01721..34fd3d2.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

416 lines
20 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: 3f01721..34fd3d2
## Commits
34fd3d2 feat: tag contenuti home cards, team, metodo, partner, agenda
## Files changed
src/components/Agenda.astro | 11 ++++---
src/components/content/TImg.astro | 2 +-
src/components/home/MethodSteps.astro | 17 +++++-----
src/components/home/PartnersStrip.astro | 5 +--
src/components/home/ProgramsGrid.astro | 5 +--
src/components/home/TeamSection.astro | 23 ++++++++------
src/components/home/TrainingCards.astro | 16 +++++-----
src/data/content-seed.ts | 55 +++++++++++++++++++++++++++++++++
src/pages/training.astro | 14 +++++----
9 files changed, 108 insertions(+), 40 deletions(-)
## Diff
diff --git a/src/components/Agenda.astro b/src/components/Agenda.astro
index db5839b..6dc3b68 100644
--- a/src/components/Agenda.astro
+++ b/src/components/Agenda.astro
@@ -1,27 +1,28 @@
---
import { agenda, disciplines, days, slots } from '../data/agenda';
+import T from './content/T.astro';
---
<section class="section section--dark agenda">
<div class="container">
- <div class="section-heading" data-reveal><h2>Agenda</h2></div>
+ <div class="section-heading" data-reveal><T tag="agenda.title" as="h2" /></div>
<div class="agenda__tabs" role="tablist" data-reveal style="--reveal-delay:.1s">
{disciplines.map((d, i) => (
- <button class:list={['agenda__tab', { 'is-active': i === 0 }]} data-filter={d} role="tab" aria-selected={i === 0 ? 'true' : 'false'}>{d}</button>
+ <button class:list={['agenda__tab', { 'is-active': i === 0 }]} data-filter={d} role="tab" aria-selected={i === 0 ? 'true' : 'false'}><T tag={`agenda.discipline.${i + 1}`} as="span" /></button>
))}
</div>
<div class="agenda__grid" data-reveal style="--reveal-delay:.2s">
<div class="agenda__corner"></div>
- {days.map((d) => <div class="agenda__day">{d}</div>)}
- {slots.map((slot) => (
+ {days.map((_, i) => <T tag={`agenda.day.${i + 1}`} as="div" class="agenda__day" />)}
+ {slots.map((slot, si) => (
<>
- <div class="agenda__slot">{slot}</div>
+ <T tag={`agenda.slot.${si + 1}`} as="div" class="agenda__slot" />
{days.map((_, dayIdx) => {
const entry = agenda.find((e) => e.day === dayIdx && e.slot === slot);
return entry
? <div class="agenda__cell" data-discipline={entry.discipline}><strong>{entry.discipline}</strong><span>{entry.time}</span><em>{entry.trainer}</em></div>
: <div class="agenda__cell agenda__cell--empty"></div>;
})}
</>
))}
</div>
</div>
diff --git a/src/components/content/TImg.astro b/src/components/content/TImg.astro
index 6c92a75..3fcb915 100644
--- a/src/components/content/TImg.astro
+++ b/src/components/content/TImg.astro
@@ -1,14 +1,14 @@
---
import { Image } from 'astro:assets';
import type { ImageMetadata } from 'astro';
import { getImageOverride } from '../../lib/content';
interface Props {
tag: string; src: ImageMetadata; alt: string;
- class?: string; loading?: 'lazy' | 'eager'; widths?: number[]; sizes?: string;
+ class?: string; loading?: 'lazy' | 'eager'; widths?: number[]; sizes?: string; height?: number;
}
const { tag, src, alt, class: cls, ...rest } = Astro.props;
const override = getImageOverride(tag);
---
{override
? <img src={override} alt={alt} class={cls} data-tag={tag} loading={rest.loading} />
: <Image src={src} alt={alt} class={cls} data-tag={tag} {...rest} />}
diff --git a/src/components/home/MethodSteps.astro b/src/components/home/MethodSteps.astro
index 2e5b670..17da6fe 100644
--- a/src/components/home/MethodSteps.astro
+++ b/src/components/home/MethodSteps.astro
@@ -1,33 +1,34 @@
---
-import { Image } from 'astro:assets';
import type { ImageMetadata } from 'astro';
-import { methodIntro, methodExtraNote, methodSteps } from '../../data/method';
+import { methodSteps } from '../../data/method';
+import T from '../content/T.astro';
+import TImg from '../content/TImg.astro';
const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/method-*.{jpg,png}', { eager: true });
const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1].default;
---
<section class="section section--alt">
<div class="container">
<div class="section-heading" data-reveal>
- <h2>Il metodo<br />InsanityLab</h2>
- <p set:html={methodIntro.replace('Metodo Insanitylab', '<strong>Metodo Insanitylab</strong>')} />
+ <T tag="home.method.title" as="h2" />
+ <T tag="method.intro" as="p" />
</div>
- <p class="method__note">{methodExtraNote}</p>
+ <T tag="method.extra-note" as="p" class="method__note" />
<div class="method__row">
{methodSteps.map((s, i) => (
<div class:list={['method__step', { 'method__step--extra': s.extra }]} data-reveal style={`--reveal-delay:${(i * 0.15).toFixed(2)}s`}>
<div class="method__circle">
- {imgOf(s.image) && <Image src={imgOf(s.image)!} alt="" widths={[220]} sizes="220px" />}
+ {imgOf(s.image) && <TImg tag={`method.step.${s.n}.image`} src={imgOf(s.image)!} alt="" widths={[220]} sizes="220px" />}
<span>{s.n}</span>
</div>
- <h3>{s.title}</h3>
- <p>{s.text}</p>
+ <T tag={`method.step.${s.n}.title`} as="h3" />
+ <T tag={`method.step.${s.n}.text`} as="p" />
</div>
))}
</div>
</div>
</section>
<style>
.method__note { text-align: right; font-size: .8rem; color: var(--c-text-light); max-width: 260px; margin-left: auto; }
.method__row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 30px; text-align: center; position: relative; }
.method__circle { position: relative; width: 150px; height: 150px; margin: 0 auto 20px; border: 1px dashed var(--c-accent-dark); border-radius: 50%; padding: 8px; }
diff --git a/src/components/home/PartnersStrip.astro b/src/components/home/PartnersStrip.astro
index db60097..fea7bf5 100644
--- a/src/components/home/PartnersStrip.astro
+++ b/src/components/home/PartnersStrip.astro
@@ -1,18 +1,19 @@
---
-import { Image } from 'astro:assets';
import type { ImageMetadata } from 'astro';
import { partners } from '../../data/partners';
+import TImg from '../content/TImg.astro';
+import { t } from '../../lib/content';
const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/partner-*.{jpg,png}', { eager: true });
const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1].default;
---
<section class="pstrip" aria-label="partner">
<div class="container pstrip__in" data-reveal="fade">
- {partners.map((p) => imgOf(p.image) && <Image src={imgOf(p.image)!} alt={p.name} height={44} />)}
+ {partners.map((p, i) => imgOf(p.image) && <TImg tag={`partner.${i + 1}.image`} src={imgOf(p.image)!} alt={t(`partner.${i + 1}.name`)} height={44} />)}
</div>
</section>
<style>
.pstrip { background: var(--c-accent); padding-block: 40px; }
.pstrip__in { display: flex; align-items: center; justify-content: space-around; gap: 30px; flex-wrap: wrap; }
.pstrip img { height: 44px; width: auto; opacity: .9; }
</style>
diff --git a/src/components/home/ProgramsGrid.astro b/src/components/home/ProgramsGrid.astro
index 564b2b8..058fd6a 100644
--- a/src/components/home/ProgramsGrid.astro
+++ b/src/components/home/ProgramsGrid.astro
@@ -1,19 +1,20 @@
---
import { programs } from '../../data/programs';
import { programIcons as icons } from '../../data/program-icons';
+import T from '../content/T.astro';
---
<section class="section">
<div class="container">
<div class="section-heading" data-reveal>
- <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="home.programs.title" as="h2" />
+ <T tag="home.programs.body" as="p" />
</div>
<div class="pgrid">
{programs.map((p, i) => (
<a class="pgrid__item" href={`/programs/${p.slug}`} data-reveal style={`--reveal-delay:${(i % 3) * 0.15}s`}>
<svg viewBox="0 0 24 24" fill="none" stroke="var(--c-accent-dark)" stroke-width="1.4" width="52" height="52" aria-hidden="true"><path d={icons[p.slug]} /></svg>
<h3>{p.title}</h3>
<p>{p.subtitle}</p>
</a>
))}
</div>
diff --git a/src/components/home/TeamSection.astro b/src/components/home/TeamSection.astro
index 64e1d19..baa0def 100644
--- a/src/components/home/TeamSection.astro
+++ b/src/components/home/TeamSection.astro
@@ -1,31 +1,36 @@
---
-import { Image } from 'astro:assets';
import type { ImageMetadata } from 'astro';
import { team, homeTeam } from '../../data/team';
+import T from '../content/T.astro';
+import TImg from '../content/TImg.astro';
+import { t } from '../../lib/content';
const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/team-*.{jpg,png}', { eager: true });
const members = homeTeam.map((short) => team.find((m) => m.shortName === short)!).filter((m) => m.image);
const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(`${name}.`))?.[1].default;
---
<section class="section">
<div class="container">
<div class="tteam__head" data-reveal>
- <h2>Team<br />Trainer</h2>
- <a class="btn" href="/about">Scopri team</a>
+ <T tag="home.team.title" as="h2" />
+ <a class="btn" href="/about"><T tag="home.team.cta" as="span" /></a>
</div>
<div class="tteam__grid">
- {members.map((m, i) => (
- <figure class="tteam__card" data-reveal style={`--reveal-delay:${i * 0.12}s`}>
- <Image src={imgOf(m.image!)!} alt={m.name} widths={[300, 500]} sizes="(max-width: 767px) 50vw, 25vw" />
- <figcaption>{m.shortName}</figcaption>
- </figure>
- ))}
+ {members.map((m, i) => {
+ const slug = m.shortName.toLowerCase();
+ return (
+ <figure class="tteam__card" data-reveal style={`--reveal-delay:${i * 0.12}s`}>
+ <TImg tag={`team.${slug}.image`} src={imgOf(m.image!)!} alt={t(`team.${slug}.name`)} widths={[300, 500]} sizes="(max-width: 767px) 50vw, 25vw" />
+ <figcaption>{m.shortName}</figcaption>
+ </figure>
+ );
+ })}
</div>
</div>
</section>
<style>
.tteam__head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 40px; }
.tteam__head h2 { text-transform: uppercase; letter-spacing: .1em; margin: 0; }
.tteam__grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; }
.tteam__card { position: relative; margin: 0; overflow: hidden; }
.tteam__card img { aspect-ratio: 3/4; object-fit: cover; width: 100%; transition: transform .5s ease; }
diff --git a/src/components/home/TrainingCards.astro b/src/components/home/TrainingCards.astro
index ee5ec91..f8937a1 100644
--- a/src/components/home/TrainingCards.astro
+++ b/src/components/home/TrainingCards.astro
@@ -1,33 +1,35 @@
---
-import { Image } from 'astro:assets';
import type { ImageMetadata } from 'astro';
import { trainings } from '../../data/trainings';
+import T from '../content/T.astro';
+import TImg from '../content/TImg.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;
const tones = ['taupe', 'tan', 'dark', 'charcoal'];
// riga 1 (training 0-1): card, foto | riga 2 (training 2-3): foto, card
const isSwapRow = (i: number) => Math.floor(i / 2) % 2 === 1;
---
<section class="tcards">
- {trainings.map((t, i) => {
- const img = imgOf(t.image);
+ {trainings.map((tr, i) => {
+ const img = imgOf(tr.image);
const card = (
<div class:list={['tcards__cell', `tcards__cell--${tones[i]}`]} data-reveal="fade" style={`--reveal-delay:${(i % 2) * 0.15}s`}>
- <h3>{t.title}</h3>
- <p>{t.caption}</p>
- <a class="tcards__link" href={t.cta.href}>Scopri →</a>
+ <T tag={`training.${tr.id}.title`} as="h3" />
+ <T tag={`training.${tr.id}.caption`} as="p" />
+ <a class="tcards__link" href={tr.cta.href}><T tag="home.training-cards.link-label" as="span" /> →</a>
</div>
);
const photo = img ? (
<div class="tcards__photo" data-reveal="fade" style={`--reveal-delay:${((i % 2) * 0.15 + 0.1).toFixed(2)}s`}>
- <Image src={img} alt={t.title} class="tcards__img" widths={[400, 700]} sizes="(max-width: 767px) 50vw, 25vw" />
+ <TImg tag={`training.${tr.id}.image`} src={img} alt={t(`training.${tr.id}.title`)} class="tcards__img" widths={[400, 700]} sizes="(max-width: 767px) 50vw, 25vw" />
</div>
) : null;
return isSwapRow(i) ? <>{photo}{card}</> : <>{card}{photo}</>;
})}
</section>
<style>
.tcards { display: grid; grid-template-columns: repeat(4, 1fr); }
.tcards__cell { padding: 48px 36px; display: flex; flex-direction: column; justify-content: center; aspect-ratio: 1; }
.tcards__cell--taupe { background: #6b5d5b; }
diff --git a/src/data/content-seed.ts b/src/data/content-seed.ts
index dfecec8..9f54a09 100644
--- a/src/data/content-seed.ts
+++ b/src/data/content-seed.ts
@@ -1,12 +1,17 @@
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';
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: '' });
@@ -90,14 +95,64 @@ export const contentSeed: SeedEntry[] = [
T('home.feature.longevity.body', 'Benessere completo tra movimento, nutrizione e stile di vita. Un percorso personalizzato che unisce prevenzione, recupero ed equilibrio. Investi nella tua salute e qualità della vita nel tempo.'),
T('home.feature.longevity.cta', 'Scopri'),
I('home.feature.performance.image'),
I('home.feature.balance.image'),
I('home.feature.longevity.image'),
// --- Home: quote banner ---
H('home.quote.text', '<strong>PERFORMANCE</strong> È SPINGERE OLTRE I PROPRI LIMITI, <strong>BALANCE</strong> È SAPERLI ASCOLTARE, <strong>LONGEVITY</strong> È COSTRUIRE NEL TEMPO CIÒ CHE DAVVERO CONTA.'),
T('home.quote.cite', 'Insanitylab', []),
I('home.quote.image'),
+
+ // --- Home: training cards (data-driven da trainings.ts) ---
+ T('home.training-cards.link-label', 'Scopri', []),
+ ...trainings.flatMap((tr) => [
+ T(`training.${tr.id}.title`, tr.title),
+ T(`training.${tr.id}.caption`, tr.caption),
+ T(`training.${tr.id}.description`, tr.description),
+ T(`training.${tr.id}.cta-label`, tr.cta.label, []),
+ I(`training.${tr.id}.image`),
+ ]),
+
+ // --- Home: programmi ---
+ T('home.programs.title', 'Programmi'),
+ T('home.programs.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.'),
+
+ // --- Home: team (data-driven da team.ts) ---
+ H('home.team.title', 'Team<br />Trainer'),
+ T('home.team.cta', 'Scopri team', []),
+ ...team.flatMap((m) => {
+ const slug = m.shortName.toLowerCase();
+ return [
+ T(`team.${slug}.name`, m.name, []),
+ T(`team.${slug}.role`, m.role, []),
+ T(`team.${slug}.bio`, m.bio),
+ I(`team.${slug}.image`),
+ I(`team.${slug}.image-about`),
+ ];
+ }),
+
+ // --- Home: metodo (data-driven da method.ts) ---
+ H('home.method.title', 'Il metodo<br />InsanityLab'),
+ H('method.intro', methodIntro.replace('Metodo Insanitylab', '<strong>Metodo Insanitylab</strong>')),
+ T('method.extra-note', methodExtraNote, []),
+ ...methodSteps.flatMap((s) => [
+ T(`method.step.${s.n}.title`, s.title),
+ T(`method.step.${s.n}.text`, s.text),
+ I(`method.step.${s.n}.image`),
+ ]),
+
+ // --- Home: partner (data-driven da partners.ts) ---
+ ...partners.flatMap((p, i) => [
+ T(`partner.${i + 1}.name`, p.name, []),
+ I(`partner.${i + 1}.image`),
+ ]),
+
+ // --- Agenda (scope limitato: titolo, discipline, giorni, slot — celle escluse) ---
+ T('agenda.title', 'Agenda'),
+ ...disciplines.map((d, i) => T(`agenda.discipline.${i + 1}`, d, [])),
+ ...days.map((d, i) => T(`agenda.day.${i + 1}`, d, [])),
+ ...slots.map((s, i) => T(`agenda.slot.${i + 1}`, s, [])),
// 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/training.astro b/src/pages/training.astro
index 7819879..7ddb083 100644
--- a/src/pages/training.astro
+++ b/src/pages/training.astro
@@ -1,42 +1,44 @@
---
import Base from '../layouts/Base.astro';
import PageHero from '../components/PageHero.astro';
import TrainingCards from '../components/home/TrainingCards.astro';
import Agenda from '../components/Agenda.astro';
import { Image } from 'astro:assets';
import { trainings } from '../data/trainings';
import trialImg from '../assets/img/trial-class.jpg';
+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 = true;
---
<Base title="Training" description="One to One, Small Groups, Performance Class e Fit Remote: trova il training InsanityLab più adatto a te.">
<PageHero title="training" />
<section class="section" style="padding-bottom:0">
<div class="section-heading">
<h2>Training</h2>
<p>Porta i tuoi allenamenti al livello successivo: programmi completi e percorsi su misura. Trova il training più adatto alle tue esigenze.</p>
</div>
</section>
<TrainingCards />
<section class="section">
<div class="container">
- {trainings.map((t, i) => (
- <div class:list={['tblock', { 'tblock--reverse': i % 2 === 1 }]} id={t.id}>
- {imgOf(t.image) && <Image src={imgOf(t.image)!} alt={t.title} widths={[500, 900]} sizes="(max-width:767px) 100vw, 45vw" data-reveal={i % 2 === 1 ? 'right' : 'left'} />}
+ {trainings.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'}>
- <h2>{t.title.toLowerCase()}</h2>
- <p>{t.description}</p>
- <a class="btn" href={t.cta.href}>{t.cta.label}</a>
+ <h2 data-tag={`training.${tr.id}.title`}>{t(`training.${tr.id}.title`).toLowerCase()}</h2>
+ <T tag={`training.${tr.id}.description`} as="p" />
+ <a class="btn" href={tr.cta.href}><T tag={`training.${tr.id}.cta-label`} as="span" /></a>
</div>
</div>
))}
</div>
</section>
<Agenda />
<section class="section section--alt">
<div class="container trial">