feat: rinomina sezione Programmi in Servizi (route, tag, migrazione DB)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,4 +10,9 @@ export default defineConfig({
|
|||||||
allowedDomains: [{ hostname: 'insanitylab.tielogic.xyz', protocol: 'https' }],
|
allowedDomains: [{ hostname: 'insanitylab.tielogic.xyz', protocol: 'https' }],
|
||||||
},
|
},
|
||||||
adapter: node({ mode: 'standalone' }),
|
adapter: node({ mode: 'standalone' }),
|
||||||
|
// La sezione "Programmi" è stata rinominata "Servizi": redirect permanenti dai vecchi URL.
|
||||||
|
redirects: {
|
||||||
|
'/programs': { status: 301, destination: '/services' },
|
||||||
|
'/programs/[slug]': { status: 301, destination: '/services/[slug]' },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ PORT=4399 node --env-file=.env ./dist/server/entry.mjs & SRV=$!
|
|||||||
trap 'kill $SRV' EXIT
|
trap 'kill $SRV' EXIT
|
||||||
sleep 2
|
sleep 2
|
||||||
fail=0
|
fail=0
|
||||||
for path in / /about /training /programs /programs/performance-class /programs/rehab /blog /contact /admin/login /robots.txt; do
|
for path in / /about /training /services /services/performance-class /services/rehab /blog /contact /admin/login /robots.txt; do
|
||||||
code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:4399$path")
|
code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:4399$path")
|
||||||
echo "$code $path"
|
echo "$code $path"
|
||||||
[ "$code" = "200" ] || fail=1
|
[ "$code" = "200" ] || fail=1
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ const year = new Date().getFullYear();
|
|||||||
<T tag="footer.col.training-title" as="h4" />
|
<T tag="footer.col.training-title" as="h4" />
|
||||||
{site.footerTraining.map((l, i) => <a href={l.href}>{t(`footer.training.${i + 1}.label`)}</a>)}
|
{site.footerTraining.map((l, i) => <a href={l.href}>{t(`footer.training.${i + 1}.label`)}</a>)}
|
||||||
</nav>
|
</nav>
|
||||||
<nav aria-label="programmi">
|
<nav aria-label="servizi">
|
||||||
<T tag="footer.col.programs-title" as="h4" />
|
<T tag="footer.col.services-title" as="h4" />
|
||||||
{site.footerPrograms.map((l, i) => <a href={l.href}>{t(`footer.programs.${i + 1}.label`)}</a>)}
|
{site.footerServices.map((l, i) => <a href={l.href}>{t(`footer.services.${i + 1}.label`)}</a>)}
|
||||||
</nav>
|
</nav>
|
||||||
<div>
|
<div>
|
||||||
<T tag="footer.col.contact-title" as="h4" />
|
<T tag="footer.col.contact-title" as="h4" />
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const blocks = [
|
|||||||
<div class="feat__text" data-reveal={b.reverse ? 'left' : 'right'}>
|
<div class="feat__text" data-reveal={b.reverse ? 'left' : 'right'}>
|
||||||
<T tag={`home.feature.${b.key}.title`} as="h2" />
|
<T tag={`home.feature.${b.key}.title`} as="h2" />
|
||||||
<T tag={`home.feature.${b.key}.body`} as="p" />
|
<T tag={`home.feature.${b.key}.body`} as="p" />
|
||||||
<a class="btn" href="/programs">{t(`home.feature.${b.key}.cta`)}</a>
|
<a class="btn" href="/services">{t(`home.feature.${b.key}.cta`)}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const slides = [
|
|||||||
<div class="container hero__in">
|
<div class="container hero__in">
|
||||||
<div class="hero__text">
|
<div class="hero__text">
|
||||||
<h1>{s.titleTags.map((tag) => <T tag={tag} as="span" class="hero__line" />)}</h1>
|
<h1>{s.titleTags.map((tag) => <T tag={tag} as="span" class="hero__line" />)}</h1>
|
||||||
<a class="btn" href="/programs">{t('home.hero.cta')}</a>
|
<a class="btn" href="/services">{t('home.hero.cta')}</a>
|
||||||
</div>
|
</div>
|
||||||
<TImg tag="home.hero.image" src={s.image} alt={s.alt} class="hero__img" widths={[600, 1000, 1400]} sizes="(max-width: 991px) 100vw, 60vw" loading="eager" />
|
<TImg tag="home.hero.image" src={s.image} alt={s.alt} class="hero__img" widths={[600, 1000, 1400]} sizes="(max-width: 991px) 100vw, 60vw" loading="eager" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const cols = await Promise.all([
|
|||||||
---
|
---
|
||||||
<section class="info" aria-label="pilastri">
|
<section class="info" aria-label="pilastri">
|
||||||
{cols.map((c, i) => (
|
{cols.map((c, i) => (
|
||||||
<a class="info__col" href="/programs" style={`background-image:url(${c.bg});--reveal-delay:${i * 0.15}s`} data-reveal="fade" data-tag={c.imageTag}>
|
<a class="info__col" href="/services" style={`background-image:url(${c.bg});--reveal-delay:${i * 0.15}s`} data-reveal="fade" data-tag={c.imageTag}>
|
||||||
<T tag={c.titleTag} as="span" class="info__title" />
|
<T tag={c.titleTag} as="span" class="info__title" />
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
---
|
---
|
||||||
import { programs } from '../../data/programs';
|
import { services } from '../../data/services';
|
||||||
import { programIcons as icons } from '../../data/program-icons';
|
import { serviceIcons as icons } from '../../data/service-icons';
|
||||||
import T from '../content/T.astro';
|
import T from '../content/T.astro';
|
||||||
---
|
---
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="section-heading" data-reveal>
|
<div class="section-heading" data-reveal>
|
||||||
<T tag="home.programs.title" as="h2" />
|
<T tag="home.services.title" as="h2" />
|
||||||
<T tag="home.programs.body" as="p" />
|
<T tag="home.services.body" as="p" />
|
||||||
</div>
|
</div>
|
||||||
<div class="pgrid">
|
<div class="pgrid">
|
||||||
{programs.map((p, i) => (
|
{services.map((p, i) => (
|
||||||
<a class="pgrid__item" href={`/programs/${p.slug}`} data-reveal style={`--reveal-delay:${(i % 3) * 0.15}s`}>
|
<a class="pgrid__item" href={`/services/${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>
|
<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>
|
||||||
<T tag={`program.${p.slug}.title`} as="h3" />
|
<T tag={`service.${p.slug}.title`} as="h3" />
|
||||||
<T tag={`program.${p.slug}.subtitle`} as="p" />
|
<T tag={`service.${p.slug}.subtitle`} as="p" />
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
+40
-40
@@ -5,7 +5,7 @@ import { team } from './team';
|
|||||||
import { methodIntro, methodExtraNote, methodSteps } from './method';
|
import { methodIntro, methodExtraNote, methodSteps } from './method';
|
||||||
import { partners } from './partners';
|
import { partners } from './partners';
|
||||||
import { disciplines, days, slots } from './agenda';
|
import { disciplines, days, slots } from './agenda';
|
||||||
import { programs } from './programs';
|
import { services } from './services';
|
||||||
import { performanceClassPricing } from './pricing';
|
import { performanceClassPricing } from './pricing';
|
||||||
|
|
||||||
export type ContentType = 'text' | 'html' | 'image';
|
export type ContentType = 'text' | 'html' | 'image';
|
||||||
@@ -40,7 +40,7 @@ export const contentSeed: SeedEntry[] = [
|
|||||||
H('footer.tagline', 'PERFORMANCE. BALANCE. LONGEVITY.'),
|
H('footer.tagline', 'PERFORMANCE. BALANCE. LONGEVITY.'),
|
||||||
T('footer.about', 'Promuoviamo il movimento come cura di sé. Abitudini sostenibili per benessere fisico e mentale.'),
|
T('footer.about', 'Promuoviamo il movimento come cura di sé. Abitudini sostenibili per benessere fisico e mentale.'),
|
||||||
T('footer.col.training-title', 'Training'),
|
T('footer.col.training-title', 'Training'),
|
||||||
T('footer.col.programs-title', 'Programmi'),
|
T('footer.col.services-title', 'Servizi'),
|
||||||
T('footer.col.contact-title', 'Contact'),
|
T('footer.col.contact-title', 'Contact'),
|
||||||
H('footer.copyright', '<strong>Insanitylab</strong>, All Rights Reserved'),
|
H('footer.copyright', '<strong>Insanitylab</strong>, All Rights Reserved'),
|
||||||
T('footer.social-label', 'Seguici', []),
|
T('footer.social-label', 'Seguici', []),
|
||||||
@@ -48,12 +48,12 @@ export const contentSeed: SeedEntry[] = [
|
|||||||
T('footer.training.2.label', site.footerTraining[1].label, []),
|
T('footer.training.2.label', site.footerTraining[1].label, []),
|
||||||
T('footer.training.3.label', site.footerTraining[2].label, []),
|
T('footer.training.3.label', site.footerTraining[2].label, []),
|
||||||
T('footer.training.4.label', site.footerTraining[3].label, []),
|
T('footer.training.4.label', site.footerTraining[3].label, []),
|
||||||
T('footer.programs.1.label', site.footerPrograms[0].label, []),
|
T('footer.services.1.label', site.footerServices[0].label, []),
|
||||||
T('footer.programs.2.label', site.footerPrograms[1].label, []),
|
T('footer.services.2.label', site.footerServices[1].label, []),
|
||||||
T('footer.programs.3.label', site.footerPrograms[2].label, []),
|
T('footer.services.3.label', site.footerServices[2].label, []),
|
||||||
T('footer.programs.4.label', site.footerPrograms[3].label, []),
|
T('footer.services.4.label', site.footerServices[3].label, []),
|
||||||
T('footer.programs.5.label', site.footerPrograms[4].label, []),
|
T('footer.services.5.label', site.footerServices[4].label, []),
|
||||||
T('footer.programs.6.label', site.footerPrograms[5].label, []),
|
T('footer.services.6.label', site.footerServices[5].label, []),
|
||||||
|
|
||||||
// --- Contact form ---
|
// --- Contact form ---
|
||||||
T('form.field.first-name', 'Nome', []),
|
T('form.field.first-name', 'Nome', []),
|
||||||
@@ -115,9 +115,9 @@ export const contentSeed: SeedEntry[] = [
|
|||||||
I(`training.${tr.id}.image`),
|
I(`training.${tr.id}.image`),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// --- Home: programmi ---
|
// --- Home: servizi ---
|
||||||
T('home.programs.title', 'Programmi'),
|
T('home.services.title', 'Servizi'),
|
||||||
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.'),
|
T('home.services.body', 'Questi sono i servizi 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) ---
|
// --- Home: team (data-driven da team.ts) ---
|
||||||
H('home.team.title', 'Team<br />Trainer'),
|
H('home.team.title', 'Team<br />Trainer'),
|
||||||
@@ -223,40 +223,40 @@ export const contentSeed: SeedEntry[] = [
|
|||||||
T('about.timeline.stage2.text', 'Contenuto in arrivo: il cliente fornirà il testo di questa tappa.'),
|
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.label', 'INSANITYLAB OGGI', []),
|
||||||
T('about.timeline.stage3.text', 'Contenuto in arrivo: il cliente fornirà il testo di questa tappa.'),
|
T('about.timeline.stage3.text', 'Contenuto in arrivo: il cliente fornirà il testo di questa tappa.'),
|
||||||
// --- Programs: lista (/programs) ---
|
// --- Services: lista (/services) ---
|
||||||
T('programs.meta.title', 'Programmi', []),
|
T('services.meta.title', 'Servizi', []),
|
||||||
T('programs.meta.description', 'I programmi InsanityLab: Personal Training, Performance Class, Coaching, Pilates Flow, Rehab e Nutrizione.', []),
|
T('services.meta.description', 'I servizi InsanityLab: Personal Training, Performance Class, Coaching, Pilates Flow, Rehab e Nutrizione.', []),
|
||||||
T('programs.hero.title', 'Programmi', []),
|
T('services.hero.title', 'Servizi', []),
|
||||||
T('programs.intro.title', 'Programmi'),
|
T('services.intro.title', 'Servizi'),
|
||||||
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('services.intro.body', "Questi sono i servizi 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ù →', []),
|
T('services.card.more-label', 'Per saperne di più →', []),
|
||||||
|
|
||||||
// --- Programs: dettaglio (data-driven da programs.ts) ---
|
// --- Services: dettaglio (data-driven da services.ts) ---
|
||||||
...programs.flatMap((p) => [
|
...services.flatMap((p) => [
|
||||||
T(`program.${p.slug}.title`, p.title),
|
T(`service.${p.slug}.title`, p.title),
|
||||||
T(`program.${p.slug}.subtitle`, p.subtitle),
|
T(`service.${p.slug}.subtitle`, p.subtitle),
|
||||||
T(`program.${p.slug}.excerpt`, p.excerpt),
|
T(`service.${p.slug}.excerpt`, p.excerpt),
|
||||||
T(`program.${p.slug}.long-description`, p.longDescription),
|
T(`service.${p.slug}.long-description`, p.longDescription),
|
||||||
...p.features.map((f, i) => T(`program.${p.slug}.feature.${i + 1}`, f)),
|
...p.features.map((f, i) => T(`service.${p.slug}.feature.${i + 1}`, f)),
|
||||||
...p.faq.flatMap((item, i) => [
|
...p.faq.flatMap((item, i) => [
|
||||||
T(`program.${p.slug}.faq.${i + 1}.q`, item.q),
|
T(`service.${p.slug}.faq.${i + 1}.q`, item.q),
|
||||||
T(`program.${p.slug}.faq.${i + 1}.a`, item.a),
|
T(`service.${p.slug}.faq.${i + 1}.a`, item.a),
|
||||||
]),
|
]),
|
||||||
I(`program.${p.slug}.image`),
|
I(`service.${p.slug}.image`),
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// --- Programs: dettaglio, template ---
|
// --- Services: dettaglio, template ---
|
||||||
T('program.side.categories-label', 'Categorie'),
|
T('service.side.categories-label', 'Categorie'),
|
||||||
T('program.side.category.1', 'Fitness', []),
|
T('service.side.category.1', 'Fitness', []),
|
||||||
T('program.side.category.2', 'Salute', []),
|
T('service.side.category.2', 'Salute', []),
|
||||||
T('program.side.category.3', 'Stile di vita', []),
|
T('service.side.category.3', 'Stile di vita', []),
|
||||||
T('program.side.category.4', 'Nutrizione', []),
|
T('service.side.category.4', 'Nutrizione', []),
|
||||||
T('program.side.others-label', 'Altri programmi'),
|
T('service.side.others-label', 'Altri servizi'),
|
||||||
H('program.pricing.empty-note', 'Contattaci per i dettagli di questo programma. <a href="/contact">Scrivici →</a>'),
|
H('service.pricing.empty-note', 'Contattaci per i dettagli di questo servizio. <a href="/contact">Scrivici →</a>'),
|
||||||
T('program.pricing.per', 'al mese, semestrale', []),
|
T('service.pricing.per', 'al mese, semestrale', []),
|
||||||
T('program.pricing.entries-singular', 'ingresso settimanale', []),
|
T('service.pricing.entries-singular', 'ingresso settimanale', []),
|
||||||
T('program.pricing.entries-plural', 'ingressi settimanali', []),
|
T('service.pricing.entries-plural', 'ingressi settimanali', []),
|
||||||
T('program.pricing.buy-cta', 'Acquista', []),
|
T('service.pricing.buy-cta', 'Acquista', []),
|
||||||
|
|
||||||
// --- Pricing performance class (data-driven da pricing.ts; monthly/entries restano nel data file) ---
|
// --- Pricing performance class (data-driven da pricing.ts; monthly/entries restano nel data file) ---
|
||||||
T('pricing.heading', performanceClassPricing.heading),
|
T('pricing.heading', performanceClassPricing.heading),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Path SVG (stroke, viewBox 0 0 24 24) delle icone programmi, condivisi tra griglia home e sidebar
|
// Path SVG (stroke, viewBox 0 0 24 24) delle icone servizi, condivisi tra griglia home e sidebar
|
||||||
export const programIcons: Record<string, string> = {
|
export const serviceIcons: Record<string, string> = {
|
||||||
'personal-training': 'M4 12h3m10 0h3M9 8v8m6-8v8M7 10v4m10-4v4',
|
'personal-training': 'M4 12h3m10 0h3M9 8v8m6-8v8M7 10v4m10-4v4',
|
||||||
'performance-class': 'M13 3l-2 7h4l-6 11 2-8H7l4-10z',
|
'performance-class': 'M13 3l-2 7h4l-6 11 2-8H7l4-10z',
|
||||||
'coaching': 'M12 3a4 4 0 110 8 4 4 0 010-8zm-7 18a7 7 0 0114 0',
|
'coaching': 'M12 3a4 4 0 110 8 4 4 0 010-8zm-7 18a7 7 0 0114 0',
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface Program {
|
export interface Service {
|
||||||
slug: string;
|
slug: string;
|
||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
@@ -9,7 +9,7 @@ export interface Program {
|
|||||||
hasPricing: boolean;
|
hasPricing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const programs: Program[] = [
|
export const services: Service[] = [
|
||||||
{
|
{
|
||||||
slug: "personal-training",
|
slug: "personal-training",
|
||||||
title: "Personal Training",
|
title: "Personal Training",
|
||||||
@@ -99,6 +99,6 @@ export const programs: Program[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getProgram(slug: string): Program | undefined {
|
export function getService(slug: string): Service | undefined {
|
||||||
return programs.find((p) => p.slug === slug);
|
return services.find((p) => p.slug === slug);
|
||||||
}
|
}
|
||||||
+8
-8
@@ -13,7 +13,7 @@ export const site = {
|
|||||||
navigation: [
|
navigation: [
|
||||||
{ label: "Insanitylab", href: "/about" },
|
{ label: "Insanitylab", href: "/about" },
|
||||||
{ label: "Training", href: "/training" },
|
{ label: "Training", href: "/training" },
|
||||||
{ label: "Programmi", href: "/programs" },
|
{ label: "Servizi", href: "/services" },
|
||||||
{ label: "Blog & Edugo", href: "/blog" },
|
{ label: "Blog & Edugo", href: "/blog" },
|
||||||
{ label: "Contatti", href: "/contact" },
|
{ label: "Contatti", href: "/contact" },
|
||||||
],
|
],
|
||||||
@@ -23,12 +23,12 @@ export const site = {
|
|||||||
{ label: "Performance class", href: "/training#performance-class" },
|
{ label: "Performance class", href: "/training#performance-class" },
|
||||||
{ label: "Fit remote", href: "/training#fit-remote" },
|
{ label: "Fit remote", href: "/training#fit-remote" },
|
||||||
],
|
],
|
||||||
footerPrograms: [
|
footerServices: [
|
||||||
{ label: "Personal Training", href: "/programs/personal-training" },
|
{ label: "Personal Training", href: "/services/personal-training" },
|
||||||
{ label: "Performance Class", href: "/programs/performance-class" },
|
{ label: "Performance Class", href: "/services/performance-class" },
|
||||||
{ label: "Coaching", href: "/programs/coaching" },
|
{ label: "Coaching", href: "/services/coaching" },
|
||||||
{ label: "Rehab", href: "/programs/rehab" },
|
{ label: "Rehab", href: "/services/rehab" },
|
||||||
{ label: "Pilates Flow", href: "/programs/pilates-flow" },
|
{ label: "Pilates Flow", href: "/services/pilates-flow" },
|
||||||
{ label: "Nutrizione", href: "/programs/nutrition" },
|
{ label: "Nutrizione", href: "/services/nutrition" },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export const trainings = [
|
export const trainings = [
|
||||||
{ id: "one-to-one", title: "One to One", caption: "Allenamento con un trainer dedicato", image: "training-one-to-one", description: "Allenati con un trainer dedicato che costruisce un percorso su misura per i tuoi obiettivi. Ogni sessione one to one è pensata per migliorare performance, tecnica e risultati. Supporto costante, attenzione totale e allenamenti personalizzati per dare il massimo.", cta: { label: "Contattaci", href: "/contact" } },
|
{ id: "one-to-one", title: "One to One", caption: "Allenamento con un trainer dedicato", image: "training-one-to-one", description: "Allenati con un trainer dedicato che costruisce un percorso su misura per i tuoi obiettivi. Ogni sessione one to one è pensata per migliorare performance, tecnica e risultati. Supporto costante, attenzione totale e allenamenti personalizzati per dare il massimo.", cta: { label: "Contattaci", href: "/contact" } },
|
||||||
{ id: "small-groups", title: "Small Groups", caption: "Mini gruppi (max 3 persone) con trainer dedicato", image: "training-small-groups", description: "Allenati in mini gruppi da massimo 3 persone con un trainer dedicato. Un’esperienza small group che unisce attenzione personalizzata, motivazione e condivisione. Programmi mirati e supporto costante per raggiungere i tuoi obiettivi insieme.", cta: { label: "Contattaci", href: "/contact" } },
|
{ id: "small-groups", title: "Small Groups", caption: "Mini gruppi (max 3 persone) con trainer dedicato", image: "training-small-groups", description: "Allenati in mini gruppi da massimo 3 persone con un trainer dedicato. Un’esperienza small group che unisce attenzione personalizzata, motivazione e condivisione. Programmi mirati e supporto costante per raggiungere i tuoi obiettivi insieme.", cta: { label: "Contattaci", href: "/contact" } },
|
||||||
{ id: "performance-class", title: "Performance Class", caption: "Classi fino a 8 persone con trainer certificato", image: "training-performance-class", description: "Allenati in classi fino a 8 persone guidate da trainer certificati. Sessioni dinamiche ad alta intensità pensate per migliorare forza, resistenza e performance. Energia, motivazione e allenamenti strutturati per dare il massimo in ogni workout.", cta: { label: "Scopri", href: "/programs/performance-class" } },
|
{ id: "performance-class", title: "Performance Class", caption: "Classi fino a 8 persone con trainer certificato", image: "training-performance-class", description: "Allenati in classi fino a 8 persone guidate da trainer certificati. Sessioni dinamiche ad alta intensità pensate per migliorare forza, resistenza e performance. Energia, motivazione e allenamenti strutturati per dare il massimo in ogni workout.", cta: { label: "Scopri", href: "/services/performance-class" } },
|
||||||
{ id: "fit-remote", title: "Fit Remote", caption: "Piano di allenamento su misura, ovunque ti alleni.", image: "training-fit-remote", description: "Con Fit Remote hai un piano di allenamento su misura, ovunque ti alleni. Programmi personalizzati creati sui tuoi obiettivi, da seguire in palestra, a casa o in viaggio. Supporto costante e allenamenti pensati per adattarsi al tuo stile di vita.", cta: { label: "Contattaci", href: "/contact" } },
|
{ id: "fit-remote", title: "Fit Remote", caption: "Piano di allenamento su misura, ovunque ti alleni.", image: "training-fit-remote", description: "Con Fit Remote hai un piano di allenamento su misura, ovunque ti alleni. Programmi personalizzati creati sui tuoi obiettivi, da seguire in palestra, a casa o in viaggio. Supporto costante e allenamenti pensati per adattarsi al tuo stile di vita.", cta: { label: "Contattaci", href: "/contact" } },
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -50,6 +50,52 @@ export function createDb(path?: string): Database.Database {
|
|||||||
if (!userCols.some((c) => c.name === 'role')) {
|
if (!userCols.some((c) => c.name === 'role')) {
|
||||||
db.exec(`ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'admin'`);
|
db.exec(`ALTER TABLE users ADD COLUMN role TEXT NOT NULL DEFAULT 'admin'`);
|
||||||
}
|
}
|
||||||
|
// Migrazione: la sezione "Programmi" è diventata "Servizi" → rinomina dei tag esistenti
|
||||||
|
// preservando le modifiche fatte dal pannello. Ordine: prima i prefissi più specifici.
|
||||||
|
// UPDATE OR IGNORE salta le righe in conflitto (tag nuovo già presente); la DELETE
|
||||||
|
// successiva ripulisce le righe vecchie rimaste. Idempotente: al secondo giro nessun match.
|
||||||
|
const TAG_RENAMES: [string, string][] = [
|
||||||
|
['home.programs.', 'home.services.'],
|
||||||
|
['footer.programs.', 'footer.services.'],
|
||||||
|
['programs.', 'services.'],
|
||||||
|
['program.', 'service.'],
|
||||||
|
];
|
||||||
|
const renameUpd = db.prepare('UPDATE OR IGNORE content_blocks SET tag = ? || substr(tag, ?) WHERE tag LIKE ?');
|
||||||
|
const renameDel = db.prepare('DELETE FROM content_blocks WHERE tag LIKE ?');
|
||||||
|
const renameTx = db.transaction(() => {
|
||||||
|
for (const [oldP, newP] of TAG_RENAMES) {
|
||||||
|
renameUpd.run(newP, oldP.length + 1, `${oldP}%`);
|
||||||
|
renameDel.run(`${oldP}%`);
|
||||||
|
}
|
||||||
|
// Tag singolo, non coperto dai prefissi: uguaglianza esatta.
|
||||||
|
db.prepare("UPDATE OR IGNORE content_blocks SET tag = 'footer.col.services-title' WHERE tag = 'footer.col.programs-title'").run();
|
||||||
|
db.prepare("DELETE FROM content_blocks WHERE tag = 'footer.col.programs-title'").run();
|
||||||
|
|
||||||
|
// I valori di default che nominavano la sezione cambiano con la rinomina. Il seed è
|
||||||
|
// INSERT OR IGNORE e non tocca le righe esistenti, quindi si aggiornano qui — ma solo
|
||||||
|
// le righe rimaste identiche al valore originale del seed (mai modificate dal pannello):
|
||||||
|
// il nuovo valore è quello del seed corrente per lo stesso tag.
|
||||||
|
const OLD_SECTION_DEFAULTS: [string, string][] = [
|
||||||
|
['global.nav.3.label', 'Programmi'],
|
||||||
|
['footer.col.services-title', 'Programmi'],
|
||||||
|
['home.services.title', 'Programmi'],
|
||||||
|
['home.services.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.'],
|
||||||
|
['services.meta.title', 'Programmi'],
|
||||||
|
['services.meta.description', 'I programmi InsanityLab: Personal Training, Performance Class, Coaching, Pilates Flow, Rehab e Nutrizione.'],
|
||||||
|
['services.hero.title', 'Programmi'],
|
||||||
|
['services.intro.title', 'Programmi'],
|
||||||
|
["services.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."],
|
||||||
|
['service.side.others-label', 'Altri programmi'],
|
||||||
|
['service.pricing.empty-note', 'Contattaci per i dettagli di questo programma. <a href="/contact">Scrivici →</a>'],
|
||||||
|
];
|
||||||
|
const seedValue = new Map(contentSeed.map((e) => [e.tag, e.value]));
|
||||||
|
const updValue = db.prepare('UPDATE content_blocks SET value = ? WHERE tag = ? AND value = ?');
|
||||||
|
for (const [tag, oldValue] of OLD_SECTION_DEFAULTS) {
|
||||||
|
const next = seedValue.get(tag);
|
||||||
|
if (next !== undefined) updValue.run(next, tag, oldValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
renameTx();
|
||||||
const ins = db.prepare('INSERT OR IGNORE INTO content_blocks (tag, type, value) VALUES (?, ?, ?)');
|
const ins = db.prepare('INSERT OR IGNORE INTO content_blocks (tag, type, value) VALUES (?, ?, ?)');
|
||||||
const syncTx = db.transaction(() => { for (const e of contentSeed) ins.run(e.tag, e.type, e.value); });
|
const syncTx = db.transaction(() => { for (const e of contentSeed) ins.run(e.tag, e.type, e.value); });
|
||||||
syncTx();
|
syncTx();
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const prerender = false;
|
|||||||
<div>
|
<div>
|
||||||
<T tag="about.direzione.title" as="h2" />
|
<T tag="about.direzione.title" as="h2" />
|
||||||
<T tag="about.direzione.body" as="p" />
|
<T tag="about.direzione.body" as="p" />
|
||||||
<a class="btn" href="/programs">{t('about.direzione.cta')}</a>
|
<a class="btn" href="/services">{t('about.direzione.cta')}</a>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<T tag="about.approccio.title" as="h2" />
|
<T tag="about.approccio.title" as="h2" />
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ export const prerender = false;
|
|||||||
const GROUP_BY_PREFIX: Record<string, string> = {
|
const GROUP_BY_PREFIX: Record<string, string> = {
|
||||||
home: 'Home',
|
home: 'Home',
|
||||||
about: 'About', team: 'About', method: 'About',
|
about: 'About', team: 'About', method: 'About',
|
||||||
program: 'Programmi', programs: 'Programmi', pricing: 'Programmi',
|
service: 'Servizi', services: 'Servizi', pricing: 'Servizi',
|
||||||
training: 'Training',
|
training: 'Training',
|
||||||
contact: 'Contatti', form: 'Contatti',
|
contact: 'Contatti', form: 'Contatti',
|
||||||
global: 'Globali', header: 'Globali', footer: 'Globali',
|
global: 'Globali', header: 'Globali', footer: 'Globali',
|
||||||
agenda: 'Globali', partner: 'Globali', notfound: 'Globali',
|
agenda: 'Globali', partner: 'Globali', notfound: 'Globali',
|
||||||
};
|
};
|
||||||
const GROUP_ORDER = ['Home', 'About', 'Programmi', 'Training', 'Contatti', 'Globali', 'Altro'];
|
const GROUP_ORDER = ['Home', 'About', 'Servizi', 'Training', 'Contatti', 'Globali', 'Altro'];
|
||||||
|
|
||||||
interface Row { tag: string; type: string; value: string; styles: string; updated_at: string; updated_by: string | null }
|
interface Row { tag: string; type: string; value: string; styles: string; updated_at: string; updated_by: string | null }
|
||||||
const rows = getDb().prepare(
|
const rows = getDb().prepare(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import InfoColumns from '../components/home/InfoColumns.astro';
|
|||||||
import FeatureBlocks from '../components/home/FeatureBlocks.astro';
|
import FeatureBlocks from '../components/home/FeatureBlocks.astro';
|
||||||
import QuoteBanner from '../components/home/QuoteBanner.astro';
|
import QuoteBanner from '../components/home/QuoteBanner.astro';
|
||||||
import TrainingCards from '../components/home/TrainingCards.astro';
|
import TrainingCards from '../components/home/TrainingCards.astro';
|
||||||
import ProgramsGrid from '../components/home/ProgramsGrid.astro';
|
import ServicesGrid from '../components/home/ServicesGrid.astro';
|
||||||
import Agenda from '../components/Agenda.astro';
|
import Agenda from '../components/Agenda.astro';
|
||||||
import TeamSection from '../components/home/TeamSection.astro';
|
import TeamSection from '../components/home/TeamSection.astro';
|
||||||
import MethodSteps from '../components/home/MethodSteps.astro';
|
import MethodSteps from '../components/home/MethodSteps.astro';
|
||||||
@@ -26,7 +26,7 @@ export const prerender = false;
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<TrainingCards />
|
<TrainingCards />
|
||||||
<ProgramsGrid />
|
<ServicesGrid />
|
||||||
<Agenda />
|
<Agenda />
|
||||||
<TeamSection />
|
<TeamSection />
|
||||||
<MethodSteps />
|
<MethodSteps />
|
||||||
|
|||||||
@@ -2,66 +2,66 @@
|
|||||||
import Base from '../../layouts/Base.astro';
|
import Base from '../../layouts/Base.astro';
|
||||||
import PageHero from '../../components/PageHero.astro';
|
import PageHero from '../../components/PageHero.astro';
|
||||||
import type { ImageMetadata } from 'astro';
|
import type { ImageMetadata } from 'astro';
|
||||||
import { programs, getProgram } from '../../data/programs';
|
import { services, getService } from '../../data/services';
|
||||||
import { performanceClassPricing } from '../../data/pricing';
|
import { performanceClassPricing } from '../../data/pricing';
|
||||||
import { programIcons } from '../../data/program-icons';
|
import { serviceIcons } from '../../data/service-icons';
|
||||||
import T from '../../components/content/T.astro';
|
import T from '../../components/content/T.astro';
|
||||||
import TImg from '../../components/content/TImg.astro';
|
import TImg from '../../components/content/TImg.astro';
|
||||||
import { t } from '../../lib/content';
|
import { t } from '../../lib/content';
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
const program = getProgram(Astro.params.slug!);
|
const service = getService(Astro.params.slug!);
|
||||||
if (!program) return new Response(null, { status: 404 });
|
if (!service) return new Response(null, { status: 404 });
|
||||||
const slug = program.slug;
|
const slug = service.slug;
|
||||||
const others = programs.filter((p) => p.slug !== program.slug);
|
const others = services.filter((p) => p.slug !== service.slug);
|
||||||
const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/program-*.jpg', { eager: true });
|
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 imgOf = (slug: string) => Object.entries(images).find(([p]) => p.includes(`program-${slug}.`))?.[1]?.default;
|
||||||
const heroImg = imgOf(program.slug);
|
const heroImg = imgOf(service.slug);
|
||||||
// Categorie come da Figma (Performance class.pdf), non quelle del blog → tag program.side.category.1-4
|
// Categorie come da Figma (Performance class.pdf), non quelle del blog → tag service.side.category.1-4
|
||||||
const categoryIndexes = [1, 2, 3, 4];
|
const categoryIndexes = [1, 2, 3, 4];
|
||||||
// Ordine card pricing come da Figma: tan, taupe, dark
|
// Ordine card pricing come da Figma: tan, taupe, dark
|
||||||
const tones = ['tan', 'taupe', 'dark'];
|
const tones = ['tan', 'taupe', 'dark'];
|
||||||
---
|
---
|
||||||
<Base title={t(`program.${slug}.title`)} description={t(`program.${slug}.excerpt`)}>
|
<Base title={t(`service.${slug}.title`)} description={t(`service.${slug}.excerpt`)}>
|
||||||
<PageHero title={t(`program.${slug}.title`).toUpperCase()} bgImage={heroImg} />
|
<PageHero title={t(`service.${slug}.title`).toUpperCase()} bgImage={heroImg} />
|
||||||
|
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="container prog">
|
<div class="container prog">
|
||||||
<div>
|
<div>
|
||||||
<h2>{t(`program.${slug}.title`).toUpperCase()}</h2>
|
<h2>{t(`service.${slug}.title`).toUpperCase()}</h2>
|
||||||
<T tag={`program.${slug}.long-description`} as="p" />
|
<T tag={`service.${slug}.long-description`} as="p" />
|
||||||
<ul class="prog__features">
|
<ul class="prog__features">
|
||||||
{program.features.map((_, i) => <T tag={`program.${slug}.feature.${i + 1}`} as="li" />)}
|
{service.features.map((_, i) => <T tag={`service.${slug}.feature.${i + 1}`} as="li" />)}
|
||||||
</ul>
|
</ul>
|
||||||
{heroImg && (
|
{heroImg && (
|
||||||
<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" />
|
<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" />
|
||||||
)}
|
)}
|
||||||
<div class="prog__faq">
|
<div class="prog__faq">
|
||||||
{program.faq.map((_, i) => (
|
{service.faq.map((_, i) => (
|
||||||
<details open={i === 0}>
|
<details open={i === 0}>
|
||||||
<summary><T tag={`program.${slug}.faq.${i + 1}.q`} /></summary>
|
<summary><T tag={`service.${slug}.faq.${i + 1}.q`} /></summary>
|
||||||
<T tag={`program.${slug}.faq.${i + 1}.a`} as="p" />
|
<T tag={`service.${slug}.faq.${i + 1}.a`} as="p" />
|
||||||
</details>
|
</details>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<aside class="prog__side">
|
<aside class="prog__side">
|
||||||
<T tag="program.side.categories-label" as="h3" />
|
<T tag="service.side.categories-label" as="h3" />
|
||||||
{categoryIndexes.map((n) => <T tag={`program.side.category.${n}`} as="p" class="prog__cat" />)}
|
{categoryIndexes.map((n) => <T tag={`service.side.category.${n}`} as="p" class="prog__cat" />)}
|
||||||
<T tag="program.side.others-label" as="h3" />
|
<T tag="service.side.others-label" as="h3" />
|
||||||
{others.map((o) => (
|
{others.map((o) => (
|
||||||
<a class="prog__other" href={`/programs/${o.slug}`}>
|
<a class="prog__other" href={`/services/${o.slug}`}>
|
||||||
<span class="prog__icon">
|
<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>
|
<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>
|
</span>
|
||||||
<T tag={`program.${o.slug}.title`} />
|
<T tag={`service.${o.slug}.title`} />
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{program.hasPricing && (
|
{service.hasPricing && (
|
||||||
<section class="section section--alt">
|
<section class="section section--alt">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
@@ -72,18 +72,18 @@ const tones = ['tan', 'taupe', 'dark'];
|
|||||||
<details class="price__group" open={g.open}>
|
<details class="price__group" open={g.open}>
|
||||||
<summary>{g.open ? '+' : '–'} <T tag={`pricing.group.${gi + 1}.label`} /></summary>
|
<summary>{g.open ? '+' : '–'} <T tag={`pricing.group.${gi + 1}.label`} /></summary>
|
||||||
{g.plans.length === 0 ? (
|
{g.plans.length === 0 ? (
|
||||||
<T tag="program.pricing.empty-note" as="p" class="price__empty" />
|
<T tag="service.pricing.empty-note" as="p" class="price__empty" />
|
||||||
) : (
|
) : (
|
||||||
<div class="price__row">
|
<div class="price__row">
|
||||||
{g.plans.map((p, i) => (
|
{g.plans.map((p, i) => (
|
||||||
<div class:list={['price__card', `price__card--${tones[i % 3]}`]}>
|
<div class:list={['price__card', `price__card--${tones[i % 3]}`]}>
|
||||||
<p class="price__amount">{p.monthly}€</p>
|
<p class="price__amount">{p.monthly}€</p>
|
||||||
<T tag="program.pricing.per" as="p" class="price__per" />
|
<T tag="service.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>
|
<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}.note`} as="p" />
|
||||||
<T tag={`pricing.group.${gi + 1}.plan.${i + 1}.installments`} 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" />
|
<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>
|
<a class="btn btn--light" href="/contact">{t('service.pricing.buy-cta')}</a>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import Base from '../../layouts/Base.astro';
|
import Base from '../../layouts/Base.astro';
|
||||||
import PageHero from '../../components/PageHero.astro';
|
import PageHero from '../../components/PageHero.astro';
|
||||||
import type { ImageMetadata } from 'astro';
|
import type { ImageMetadata } from 'astro';
|
||||||
import { programs } from '../../data/programs';
|
import { services } from '../../data/services';
|
||||||
import T from '../../components/content/T.astro';
|
import T from '../../components/content/T.astro';
|
||||||
import TImg from '../../components/content/TImg.astro';
|
import TImg from '../../components/content/TImg.astro';
|
||||||
import { t } from '../../lib/content';
|
import { t } from '../../lib/content';
|
||||||
@@ -10,27 +10,27 @@ const images = import.meta.glob<{ default: ImageMetadata }>('../../assets/img/pr
|
|||||||
const imgOf = (slug: string) => Object.entries(images).find(([p]) => p.includes(`program-${slug}.`))?.[1]?.default;
|
const imgOf = (slug: string) => Object.entries(images).find(([p]) => p.includes(`program-${slug}.`))?.[1]?.default;
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
---
|
---
|
||||||
<Base title={t('programs.meta.title')} description={t('programs.meta.description')}>
|
<Base title={t('services.meta.title')} description={t('services.meta.description')}>
|
||||||
<PageHero title={t('programs.hero.title')} />
|
<PageHero title={t('services.hero.title')} />
|
||||||
<section class="section">
|
<section class="section">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<T tag="programs.intro.title" as="h2" />
|
<T tag="services.intro.title" as="h2" />
|
||||||
<T tag="programs.intro.body" as="p" />
|
<T tag="services.intro.body" as="p" />
|
||||||
</div>
|
</div>
|
||||||
<div class="plist">
|
<div class="plist">
|
||||||
{programs.map((p, i) => (
|
{services.map((p, i) => (
|
||||||
<article class="plist__card" data-reveal style={`--reveal-delay:${((i % 3) * 0.15).toFixed(2)}s`}>
|
<article class="plist__card" data-reveal style={`--reveal-delay:${((i % 3) * 0.15).toFixed(2)}s`}>
|
||||||
{imgOf(p.slug) && (
|
{imgOf(p.slug) && (
|
||||||
<a class="plist__photo" href={`/programs/${p.slug}`}>
|
<a class="plist__photo" href={`/services/${p.slug}`}>
|
||||||
<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" />
|
<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>
|
</a>
|
||||||
)}
|
)}
|
||||||
<div class="plist__body">
|
<div class="plist__body">
|
||||||
<T tag={`program.${p.slug}.title`} as="h3" />
|
<T tag={`service.${p.slug}.title`} as="h3" />
|
||||||
<T tag={`program.${p.slug}.subtitle`} as="p" class="plist__sub" />
|
<T tag={`service.${p.slug}.subtitle`} as="p" class="plist__sub" />
|
||||||
<T tag={`program.${p.slug}.excerpt`} as="p" />
|
<T tag={`service.${p.slug}.excerpt`} as="p" />
|
||||||
<a class="plist__more" href={`/programs/${p.slug}`}>{t('programs.card.more-label')}</a>
|
<a class="plist__more" href={`/services/${p.slug}`}>{t('services.card.more-label')}</a>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import { getDb } from '../lib/db';
|
import { getDb } from '../lib/db';
|
||||||
import { programs } from '../data/programs';
|
import { services } from '../data/services';
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
const STATIC = ['/', '/about', '/contact', '/training', '/programs', '/blog'];
|
const STATIC = ['/', '/about', '/contact', '/training', '/services', '/blog'];
|
||||||
|
|
||||||
export const GET: APIRoute = ({ site }) => {
|
export const GET: APIRoute = ({ site }) => {
|
||||||
const base = (site ?? new URL('https://insanitylab.tielogic.xyz')).toString().replace(/\/$/, '');
|
const base = (site ?? new URL('https://insanitylab.tielogic.xyz')).toString().replace(/\/$/, '');
|
||||||
const posts = getDb().prepare('SELECT slug FROM posts WHERE draft = 0').all() as { slug: string }[];
|
const posts = getDb().prepare('SELECT slug FROM posts WHERE draft = 0').all() as { slug: string }[];
|
||||||
const urls = [
|
const urls = [
|
||||||
...STATIC,
|
...STATIC,
|
||||||
...programs.map((p) => `/programs/${p.slug}`),
|
...services.map((p) => `/services/${p.slug}`),
|
||||||
...posts.map((p) => `/blog/${p.slug}`),
|
...posts.map((p) => `/blog/${p.slug}`),
|
||||||
];
|
];
|
||||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.map((u) => ` <url><loc>${base}${u}</loc></url>`).join('\n')}\n</urlset>`;
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls.map((u) => ` <url><loc>${base}${u}</loc></url>`).join('\n')}\n</urlset>`;
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { mkdtempSync, rmSync } from 'node:fs';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { join } from 'node:path';
|
||||||
import { createDb } from '../src/lib/db';
|
import { createDb } from '../src/lib/db';
|
||||||
|
|
||||||
describe('schema content_blocks e ruoli', () => {
|
describe('schema content_blocks e ruoli', () => {
|
||||||
@@ -24,6 +27,60 @@ describe('schema content_blocks e ruoli', () => {
|
|||||||
expect(row.role).toBe('admin');
|
expect(row.role).toBe('admin');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('migra i tag programmi → servizi preservando i valori modificati dal pannello', () => {
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), 'il-db-test-'));
|
||||||
|
const path = join(dir, 'test.db');
|
||||||
|
try {
|
||||||
|
// Simula un DB di produzione pre-rinomina: tag vecchi con valori modificati dal pannello.
|
||||||
|
const db1 = createDb(path);
|
||||||
|
const old = db1.transaction(() => {
|
||||||
|
db1.prepare("DELETE FROM content_blocks WHERE tag IN ('home.services.title', 'service.rehab.title', 'services.hero.title', 'services.meta.title', 'footer.services.1.label', 'footer.col.services-title')").run();
|
||||||
|
const ins = db1.prepare("INSERT INTO content_blocks (tag, type, value, updated_by) VALUES (?, 'text', ?, 'editor')");
|
||||||
|
ins.run('home.programs.title', 'Titolo modificato');
|
||||||
|
ins.run('program.rehab.title', 'Rehab modificato');
|
||||||
|
ins.run('programs.hero.title', 'Hero modificato');
|
||||||
|
ins.run('footer.programs.1.label', 'Label modificata');
|
||||||
|
ins.run('footer.col.programs-title', 'Colonna modificata');
|
||||||
|
// Riga mai toccata dal pannello: valore identico al vecchio seed → deve prendere il nuovo default.
|
||||||
|
db1.prepare("INSERT INTO content_blocks (tag, type, value) VALUES ('programs.meta.title', 'text', 'Programmi')").run();
|
||||||
|
db1.prepare("UPDATE content_blocks SET value = 'Programmi' WHERE tag = 'global.nav.3.label'").run();
|
||||||
|
});
|
||||||
|
old();
|
||||||
|
db1.close();
|
||||||
|
|
||||||
|
// Riapertura: la migrazione rinomina i tag preservando valore e updated_by.
|
||||||
|
const db2 = createDb(path);
|
||||||
|
const val = (tag: string) =>
|
||||||
|
db2.prepare('SELECT value, updated_by FROM content_blocks WHERE tag = ?').get(tag) as { value: string; updated_by: string | null } | undefined;
|
||||||
|
expect(val('home.services.title')).toMatchObject({ value: 'Titolo modificato', updated_by: 'editor' });
|
||||||
|
expect(val('service.rehab.title')).toMatchObject({ value: 'Rehab modificato', updated_by: 'editor' });
|
||||||
|
expect(val('services.hero.title')).toMatchObject({ value: 'Hero modificato', updated_by: 'editor' });
|
||||||
|
expect(val('footer.services.1.label')).toMatchObject({ value: 'Label modificata', updated_by: 'editor' });
|
||||||
|
expect(val('footer.col.services-title')).toMatchObject({ value: 'Colonna modificata', updated_by: 'editor' });
|
||||||
|
|
||||||
|
// Le righe rimaste al vecchio valore di default prendono il nuovo ("Servizi").
|
||||||
|
expect(val('services.meta.title')).toMatchObject({ value: 'Servizi' });
|
||||||
|
expect(val('global.nav.3.label')).toMatchObject({ value: 'Servizi' });
|
||||||
|
// La riga rinominata ma modificata dal pannello resta invariata (già verificata sopra):
|
||||||
|
// home.services.title = 'Titolo modificato'.
|
||||||
|
|
||||||
|
// Nessun tag vecchio residuo.
|
||||||
|
const residui = db2.prepare(
|
||||||
|
"SELECT count(*) AS c FROM content_blocks WHERE tag LIKE 'program%' OR tag LIKE 'home.programs%' OR tag LIKE 'footer.programs%' OR tag = 'footer.col.programs-title'"
|
||||||
|
).get() as { c: number };
|
||||||
|
expect(residui.c).toBe(0);
|
||||||
|
|
||||||
|
// Idempotente: una terza apertura non altera i valori.
|
||||||
|
db2.close();
|
||||||
|
const db3 = createDb(path);
|
||||||
|
const again = db3.prepare('SELECT value FROM content_blocks WHERE tag = ?').get('home.services.title') as { value: string };
|
||||||
|
expect(again.value).toBe('Titolo modificato');
|
||||||
|
db3.close();
|
||||||
|
} finally {
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('migra un DB esistente senza colonna role', () => {
|
it('migra un DB esistente senza colonna role', () => {
|
||||||
// simula DB vecchio: createDb due volte sullo stesso path in-memory non è possibile,
|
// simula DB vecchio: createDb due volte sullo stesso path in-memory non è possibile,
|
||||||
// quindi si verifica che la migrazione sia idempotente (doppia esecuzione non lancia)
|
// quindi si verifica che la migrazione sia idempotente (doppia esecuzione non lancia)
|
||||||
|
|||||||
Reference in New Issue
Block a user