Servizi e landing Promo ed eventi: rinomine, pulizia contenuti e nuova pagina

Servizi:
- Personal Training rinominato "One to One" (slug /services/one-to-one) con
  redirect 301 dal vecchio URL e migrazione DB idempotente dei contenuti taggati
- Nuova pagina "Small Group" (slug /services/small-group, fuori dalla griglia)
- Rimossi i titoli intro di /training e /services e le liste feature di One to
  One, Small Group, Rehab e Osteopatia; la lista feature ora è condizionale
- Fix elenco puntato nel long-description (white-space: pre-line, selettore
  ancorato al container per gli elementi resi dal componente <T>)
- Immagine di Performance Class aggiornata
- Nella griglia Servizi i riquadri "One to One" e "Performance Class" rimandano
  a /training; nella pagina Training i riquadri One to One e Small Group aprono
  le rispettive schede servizio

Landing:
- Pagina "Promo ed eventi" (slug /promo-eventi) integrata nel sito con header e
  footer condivisi, basata sul nuovo mockup di lancio
- Card a 5 incontri con prezzi edizione apertura, FAQ ad accordion e acquisto
  tramite gestionale bsport
- Nuova voce di menu "Promo ed eventi"

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 11:33:11 +02:00
parent 5b34ee1697
commit 11fc2b3e54
14 changed files with 310 additions and 22 deletions
+256
View File
@@ -0,0 +1,256 @@
---
import Base from '../layouts/Base.astro';
// 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,
// inserisce i dati e paga via Stripe dentro la piattaforma. Link centralizzato qui:
// quando avremo gli ID membership per singolo prodotto basterà mappare BSPORT_URL card per card.
const BSPORT_URL = 'https://backoffice.bsport.io/login/signup?membership=3428';
export const prerender = false;
---
<Base title="Promo ed eventi — InsanityLab" description="Edizione apertura: pacchetti a 5 incontri per One to One, Small Group, Reformer e Holistic Class. Acquisto online, pagamento sicuro via Stripe.">
<div class="lp">
<div class="page">
<section class="hero">
<div class="badge">insanity.lab · bologna savena</div>
<h1>Allenati con metodo.</h1>
<p class="accent">Scegli come</p>
<h1>vuoi farlo.</h1>
<p class="desc">Allenati con flessibilità, senza vincoli, da subito.<br>Acquisto online in pochi secondi, pagamento sicuro via Stripe.</p>
<a class="btn-sand" href="#proposte">LE NOSTRE PROPOSTE →</a>
</section>
<section class="launch">
<div class="badge">solo per il lancio</div>
<h1>Il nostro metodo nasce per durare.</h1>
<p class="accent">Per l'apertura, un'eccezione. Solo per poco.</p>
<p class="desc">In-Sanity Lab lavora per percorsi strutturati — trimestrali, semestrali — perché i risultati veri si costruiscono nel tempo. Per la nuova apertura, ci sarà un'eccezione: 5 incontri pensati come un vero mini-percorso, non semplici lezioni sciolte.</p>
</section>
<section class="why5">
<p>Non è una prova. È un percorso in miniatura — per capire come lavoriamo prima di impegnarti su un programma trimestrale o semestrale.</p>
</section>
<section class="cards-wrap" id="proposte">
<div class="cards">
<div class="card">
<div class="eyebrow">Personal training</div>
<div class="title">One to One</div>
<div class="price-row">
<div class="price">163 €</div>
<div class="price-old">300 €</div>
</div>
<div class="unit">5 incontri</div>
<ul>
<li>Screening iniziale, definizione dei tuoi obiettivi</li>
<li>Check-up con profilazione fisica, il tuo punto di partenza</li>
<li>PT dedicato, presente a ogni sessione</li>
<li>In-sanity plan, il tuo programma personalizzato</li>
</ul>
<a class="buy" href={BSPORT_URL} target="_blank" rel="noopener">ACQUISTA →</a>
</div>
<div class="card featured">
<div class="featured-badge">Più scelto</div>
<div class="eyebrow">Personal training</div>
<div class="title">Small Group</div>
<div class="price-row">
<div class="price">110 €</div>
<div class="price-old">220 €</div>
</div>
<div class="unit">5 incontri</div>
<ul>
<li>Screening iniziale, definizione dei tuoi obiettivi</li>
<li>Check-up con profilazione fisica, il tuo punto di partenza</li>
<li>PT dedicato, presente a ogni sessione</li>
<li>In-sanity plan, il tuo programma personalizzato</li>
</ul>
<a class="buy" href={BSPORT_URL} target="_blank" rel="noopener">ACQUISTA →</a>
</div>
<div class="card">
<div class="eyebrow">Pilates class</div>
<div class="title">Reformer</div>
<div class="price-row">
<div class="price">100 €</div>
<div class="price-old">140 €</div>
</div>
<div class="unit">5 incontri</div>
<ul>
<li>Core, postura e mobilità su macchina Reformer</li>
<li>Lavoro a basso impatto sulle articolazioni</li>
<li>Percorso breve, non lezioni isolate</li>
</ul>
<a class="buy" href={BSPORT_URL} target="_blank" rel="noopener">ACQUISTA →</a>
</div>
<div class="card">
<div class="eyebrow">Holistic class</div>
<div class="title">Pilates Mat, Mobility, Yoga</div>
<div class="price-row">
<div class="price">50 €</div>
<div class="price-old">100 €</div>
</div>
<div class="unit">5 incontri</div>
<ul>
<li>Mobilità articolare, respirazione, equilibrio</li>
<li>Da solo, o integrato nei tuoi allenamenti</li>
<li>Adatto a tutti i livelli</li>
</ul>
<a class="buy" href={BSPORT_URL} target="_blank" rel="noopener">ACQUISTA →</a>
</div>
</div>
<p class="validity-note">Disponibile solo per l'apertura · Valido entro 2 mesi dal primo incontro e comunque non oltre il 1° dicembre 2026</p>
</section>
<section class="faq-wrap">
<h1>Tutto quello che vuoi sapere prima di acquistare.</h1>
<div class="faq-block">
<div class="faq-q"><p>Come funziona il pacchetto a incontri per One to One e Small Group?</p><div class="faq-toggle">+</div></div>
<p class="faq-a">Acquisti il pacchetto online con pagamento sicuro via <span class="hl">Stripe</span>. Ricevi una conferma via email. Poi ci scrivi su <span class="hl">WhatsApp (351 753 5977)</span> per prenotare le singole sessioni in base agli orari disponibili. Verrai ricontattato subito tramite messaggio o chiamata! Gli incontri sono validi 2 mesi dalla data del primo incontro e comunque non oltre il 1° dicembre 2026.</p>
</div>
<div class="faq-block">
<div class="faq-q"><p>Come funziona il percorso Reformer o Holistic Class?</p><div class="faq-toggle">+</div></div>
<p class="faq-a">Acquisti il pacchetto online con pagamento sicuro via <span class="hl">Stripe</span>. Ricevi una conferma via email. Poi vai nella schermata Home, clicca il servizio che hai acquistato e seleziona autonomamente la class nel giorno e nell'orario che desideri. In caso di problemi contattaci su <span class="hl">WhatsApp (351 753 5977)</span>! Gli incontri sono validi 2 mesi dalla data del primo incontro e comunque non oltre il 1° dicembre 2026.</p>
</div>
<div class="faq-block">
<div class="faq-q"><p>Posso usare gli incontri su servizi diversi?</p><div class="faq-toggle">+</div></div>
<p class="faq-a">Ogni pacchetto è dedicato a un singolo servizio (<span class="hl">One to One</span>, <span class="hl">Small Group</span>, <span class="hl">Reformer</span> o <span class="hl">Holistic Class</span>). Se vuoi accedere a più servizi puoi acquistare pacchetti separati per ciascuno.</p>
</div>
<div class="faq-block">
<div class="faq-q"><p>Posso regalare un pacchetto a qualcun altro?</p><div class="faq-toggle">+</div></div>
<p class="faq-a">Sì. Puoi acquistare il pacchetto a tuo nome — per la fatturazione — e farlo utilizzare a un'altra persona: perfetto se vuoi fare un regalo a un amico o un familiare. Ti basterà comunicarci chi userà gli incontri quando ci scriverai su <span class="hl">WhatsApp (351 753 5977)</span> per organizzare le sessioni.</p>
</div>
<div class="faq-block">
<div class="faq-q"><p>Il pagamento è sicuro?</p><div class="faq-toggle">+</div></div>
<p class="faq-a">Sì. Il pagamento avviene tramite <span class="hl">Stripe</span>, una delle piattaforme di pagamento online più sicure al mondo. I tuoi dati bancari non vengono mai condivisi con insanity.lab — tutto è gestito in modo cifrato da Stripe.</p>
</div>
<div class="faq-block">
<div class="faq-q"><p>Posso chiedere un rimborso?</p><div class="faq-toggle">+</div></div>
<p class="faq-a">Per richieste di rimborso o problemi con l'acquisto scrivici su <span class="hl">WhatsApp</span> o via email. Valutiamo ogni situazione in modo diretto e umano.</p>
</div>
</section>
<div class="contact-bar">
<p>Via Leandro Alberti 76 e Via De Mattiolo 5, 40139 Bologna &nbsp;·&nbsp; 351 753 5977 &nbsp;·&nbsp; info@insanitylab.it</p>
</div>
</div>
</div>
</Base>
<script>
// Accordion FAQ: click (o Invio/Spazio) sul quesito apre/chiude la risposta.
document.querySelectorAll('.lp .faq-block .faq-q').forEach((q) => {
const block = q.closest('.faq-block');
const toggle = q.querySelector('.faq-toggle');
q.setAttribute('role', 'button');
q.setAttribute('tabindex', '0');
q.setAttribute('aria-expanded', 'false');
const set = (open: boolean) => {
block?.classList.toggle('is-open', open);
q.setAttribute('aria-expanded', String(open));
if (toggle) toggle.textContent = open ? '' : '+';
};
q.addEventListener('click', () => set(!block?.classList.contains('is-open')));
q.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); set(!block?.classList.contains('is-open')); }
});
});
</script>
<style>
.lp { background: #ECE7DC; }
.lp .page { max-width: 1000px; margin: 0 auto; background: #FFFFFF; font-family: 'Inter', system-ui, Arial, sans-serif; }
/* HERO */
.lp .hero { background: #2D2D2D; padding: 72px 64px 84px; }
.lp .badge {
display: inline-block; padding: 7px 16px; border: 0.5px solid rgba(196,168,130,0.5);
border-radius: 999px; margin-bottom: 24px; font-size: 11px; letter-spacing: 0.08em;
color: #C4A882; font-weight: 500; text-transform: uppercase;
}
.lp .hero h1 { margin: 0; font-size: 44px; line-height: 1.2; font-weight: 700; color: #FFFFFF; }
.lp .hero .accent { margin: 4px 0; font-size: 38px; line-height: 1.3; font-style: italic; color: #C4A882; font-family: Georgia, serif; }
.lp .hero p.desc { margin: 28px 0 32px; font-size: 16px; line-height: 1.7; color: #B8B6AE; max-width: 480px; }
.lp .btn-sand {
display: inline-block; background: #C4A882; color: #2C2C2A; border: none; padding: 15px 30px;
border-radius: 999px; font-size: 13px; font-weight: 700; letter-spacing: 0.04em; cursor: pointer;
text-decoration: none;
}
/* LAUNCH */
.lp .launch { background: #F2ECE1; padding: 72px 64px; }
.lp .launch .badge { border-color: rgba(140,107,55,0.35); color: #8A6E3F; }
.lp .launch h1 { margin: 0; font-size: 40px; line-height: 1.25; font-weight: 700; color: #2D2D2D; }
.lp .launch .accent { margin: 8px 0 28px; font-size: 30px; line-height: 1.35; font-style: italic; color: #8A6E3F; font-family: Georgia, serif; }
.lp .launch p.desc { margin: 0; font-size: 16px; line-height: 1.7; color: #5C564A; max-width: 540px; }
/* PERCHE 5 */
.lp .why5 { padding: 48px 64px 8px; }
.lp .why5 p { margin: 0; font-size: 18px; line-height: 1.7; font-style: italic; color: #2D2D2D; font-family: Georgia, serif; max-width: 620px; }
/* CARDS */
.lp .cards-wrap { padding: 32px 64px 72px; }
.lp .cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }
.lp .card { background: #FAF8F3; border: 0.5px solid #E3DCCB; border-radius: 14px; padding: 26px; position: relative; }
.lp .card.featured { border: 2px solid #2D2D2D; }
.lp .featured-badge {
position: absolute; top: -12px; left: 50%; transform: translateX(-50%); background: #2D2D2D;
color: #FFFFFF; font-size: 11px; padding: 4px 14px; border-radius: 999px; white-space: nowrap; font-weight: 500;
}
.lp .card .eyebrow { font-size: 11px; letter-spacing: 0.08em; color: #8A8980; margin: 0 0 4px; text-transform: uppercase; font-weight: 700; }
.lp .card .title { font-size: 19px; font-weight: 700; color: #2D2D2D; margin: 0 0 14px; }
.lp .card .price-row { display: flex; align-items: baseline; gap: 10px; margin: 0 0 2px; }
.lp .card .price { font-size: 26px; font-weight: 700; color: #2D2D2D; margin: 0; }
.lp .card .price-old { font-size: 15px; color: #ADABA1; text-decoration: line-through; font-weight: 400; }
.lp .card .unit { font-size: 12px; color: #8A8980; margin: 0 0 18px; }
.lp .card ul { list-style: none; padding: 0; margin: 0 0 20px; display: flex; flex-direction: column; gap: 9px; }
.lp .card li { font-size: 13.5px; line-height: 1.5; color: #5C564A; display: flex; gap: 8px; }
.lp .card li::before { content: "—"; color: #C4A882; font-weight: 700; flex-shrink: 0; }
.lp .card .buy {
display: block; width: 100%; box-sizing: border-box; text-align: center; text-decoration: none;
background: #2D2D2D; color: #FFFFFF; border: none; padding: 13px; border-radius: 999px;
font-size: 12px; font-weight: 700; letter-spacing: 0.03em; cursor: pointer;
}
.lp .card.featured .buy { background: #C4A882; color: #2C2C2A; }
.lp .validity-note { text-align: center; font-size: 12px; color: #8A8980; margin: 22px 0 0; }
/* CONTACT BAR */
.lp .contact-bar { padding: 20px 64px 40px; text-align: center; }
.lp .contact-bar p { margin: 0; font-size: 11.5px; line-height: 1.6; color: #8A8980; }
/* FAQ */
.lp .faq-wrap { padding: 72px 64px; }
.lp .faq-wrap h1 { margin: 0 0 40px; font-size: 34px; line-height: 1.25; font-weight: 700; color: #2D2D2D; max-width: 480px; }
.lp .faq-block { border-top: 0.5px solid #E3DCCB; padding: 22px 0; }
.lp .faq-block:last-child { border-bottom: 0.5px solid #E3DCCB; }
.lp .faq-q { display: flex; justify-content: space-between; align-items: center; gap: 24px; cursor: pointer; }
.lp .faq-q p { margin: 0; font-size: 16px; font-weight: 500; color: #2D2D2D; }
.lp .faq-q:focus-visible { outline: 2px solid #8A6E3F; outline-offset: 4px; }
.lp .faq-toggle {
width: 30px; height: 30px; border-radius: 50%; border: 0.5px solid #C9C3B4; display: flex;
align-items: center; justify-content: center; flex-shrink: 0; font-size: 16px; color: #2D2D2D;
transition: background .2s ease, color .2s ease, border-color .2s ease;
}
.lp .faq-block.is-open .faq-toggle { background: #2D2D2D; color: #FFFFFF; border-color: #2D2D2D; }
.lp .faq-a { display: none; margin: 14px 0 0; font-size: 14.5px; line-height: 1.7; color: #8A8980; max-width: 620px; }
.lp .faq-block.is-open .faq-a { display: block; }
.lp .faq-a .hl { color: #8A6E3F; }
@media (max-width: 700px) {
.lp .hero, .lp .launch, .lp .why5, .lp .cards-wrap, .lp .faq-wrap, .lp .contact-bar { padding-left: 28px; padding-right: 28px; }
.lp .cards { grid-template-columns: 1fr; }
.lp .hero h1 { font-size: 32px; }
.lp .launch h1 { font-size: 28px; }
}
</style>
+7 -4
View File
@@ -29,10 +29,12 @@ const tones = ['tan', 'taupe', 'dark'];
<div class="container prog">
<div>
<h2>{t(`service.${slug}.title`).toUpperCase()}</h2>
<T tag={`service.${slug}.long-description`} as="p" />
<ul class="prog__features">
{service.features.map((_, i) => <T tag={`service.${slug}.feature.${i + 1}`} as="li" />)}
</ul>
<T tag={`service.${slug}.long-description`} as="p" class="prog__desc" />
{service.features.length > 0 && (
<ul class="prog__features">
{service.features.map((_, i) => <T tag={`service.${slug}.feature.${i + 1}`} as="li" />)}
</ul>
)}
{heroImg && (
<TImg tag={`service.${slug}.image`} src={heroImg} alt={t(`service.${slug}.title`)} class="prog__img" widths={[600, 1000]} sizes="(max-width:991px) 100vw, 60vw" data-reveal="fade" />
)}
@@ -97,6 +99,7 @@ const tones = ['tan', 'taupe', 'dark'];
<style>
.prog { display: grid; grid-template-columns: 2.2fr 1fr; gap: 60px; }
.prog :global(.prog__desc) { white-space: pre-line; }
.prog__features { padding-left: 20px; display: grid; gap: 8px; margin-bottom: 30px; }
.prog__features li::marker { color: var(--c-accent); }
.prog__img { width: 100%; height: auto; margin-bottom: 30px; }
+4 -2
View File
@@ -9,6 +9,9 @@ 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;
const gridServices = services.filter((p) => p.grid !== false);
// Questi servizi rimandano alla pagina Training invece che alla scheda di dettaglio
const TRAINING_LINK = new Set(['one-to-one', 'performance-class']);
const moreHref = (slug: string) => (TRAINING_LINK.has(slug) ? '/training' : `/services/${slug}`);
export const prerender = false;
---
<Base title={t('services.meta.title')} description={t('services.meta.description')}>
@@ -16,7 +19,6 @@ export const prerender = false;
<section class="section">
<div class="container">
<div class="section-heading">
<T tag="services.intro.title" as="h2" />
<T tag="services.intro.body" as="p" />
</div>
<div class="plist">
@@ -31,7 +33,7 @@ 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={`/services/${p.slug}`}>{t('services.card.more-label')}</a>
<a class="plist__more" href={moreHref(p.slug)}>{t('services.card.more-label')}</a>
</div>
</article>
))}
+1 -1
View File
@@ -3,7 +3,7 @@ import { getDb } from '../lib/db';
import { services } from '../data/services';
export const prerender = false;
const STATIC = ['/', '/about', '/contact', '/training', '/services', '/blog'];
const STATIC = ['/', '/about', '/contact', '/training', '/services', '/blog', '/promo-eventi'];
export const GET: APIRoute = ({ site }) => {
const base = (site ?? new URL('https://insanitylab.tielogic.xyz')).toString().replace(/\/$/, '');
-1
View File
@@ -18,7 +18,6 @@ export const prerender = false;
<section class="section" style="padding-bottom:0">
<div class="section-heading">
<T tag="training.intro.title" as="h2" />
<T tag="training.intro.subtitle" as="p" />
</div>
</section>