Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a30bc08b9 | |||
| bcc965655d | |||
| 8d593dce90 | |||
| 47a93049bb | |||
| 5ff47711cd | |||
| f48518d8fd | |||
| 30b509436e |
@@ -11,3 +11,7 @@ CONTACT_FROM=postmaster@insanitylab.it
|
|||||||
CONTACT_TO=info@insanitylab.it
|
CONTACT_TO=info@insanitylab.it
|
||||||
DB_PATH=data/insanitylab.db
|
DB_PATH=data/insanitylab.db
|
||||||
UPLOADS_DIR=uploads
|
UPLOADS_DIR=uploads
|
||||||
|
|
||||||
|
# Google Tag Manager: ID container preso da tagmanager.google.com (accanto al nome container).
|
||||||
|
# Formato GTM-XXXXXXX. Lascia vuoto per non caricare GTM (es. in locale/staging).
|
||||||
|
GTM_ID=GTM-XXXXXXX
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import { performanceClassPricing } from './pricing';
|
|||||||
export type ContentType = 'text' | 'html' | 'image';
|
export type ContentType = 'text' | 'html' | 'image';
|
||||||
export interface SeedEntry { tag: string; type: ContentType; value: string; styleOptions?: StyleGroup[] }
|
export interface SeedEntry { tag: string; type: ContentType; value: string; styleOptions?: StyleGroup[] }
|
||||||
|
|
||||||
const T = (tag: string, value: string, styleOptions: StyleGroup[] = ['size', 'color', 'weight', 'style']): SeedEntry =>
|
const T = (tag: string, value: string, styleOptions: StyleGroup[] = ['size', 'color', 'weight', 'style', 'align']): SeedEntry =>
|
||||||
({ tag, type: 'text', value, styleOptions });
|
({ tag, type: 'text', value, styleOptions });
|
||||||
const H = (tag: string, value: string, styleOptions: StyleGroup[] = ['size', 'color']): SeedEntry =>
|
const H = (tag: string, value: string, styleOptions: StyleGroup[] = ['size', 'color', 'align']): SeedEntry =>
|
||||||
({ tag, type: 'html', value, styleOptions });
|
({ tag, type: 'html', value, styleOptions });
|
||||||
const I = (tag: string): SeedEntry => ({ tag, type: 'image', value: '' });
|
const I = (tag: string): SeedEntry => ({ tag, type: 'image', value: '' });
|
||||||
|
|
||||||
@@ -24,6 +24,7 @@ export const contentSeed: SeedEntry[] = [
|
|||||||
T('global.contact.address', site.address, []),
|
T('global.contact.address', site.address, []),
|
||||||
T('global.contact.city-line', site.cityLine, []),
|
T('global.contact.city-line', site.cityLine, []),
|
||||||
T('global.contact.map-query', site.mapQuery, []),
|
T('global.contact.map-query', site.mapQuery, []),
|
||||||
|
T('global.whatsapp', '351 753 5977', []),
|
||||||
T('global.hours.1', site.hours[0], []),
|
T('global.hours.1', site.hours[0], []),
|
||||||
T('global.hours.2', site.hours[1], []),
|
T('global.hours.2', site.hours[1], []),
|
||||||
T('global.nav.1.label', site.navigation[0].label, []),
|
T('global.nav.1.label', site.navigation[0].label, []),
|
||||||
@@ -299,6 +300,79 @@ export const contentSeed: SeedEntry[] = [
|
|||||||
T('notfound.body', 'La pagina che cerchi non esiste o è stata spostata.'),
|
T('notfound.body', 'La pagina che cerchi non esiste o è stata spostata.'),
|
||||||
T('notfound.cta', 'Torna alla home', []),
|
T('notfound.cta', 'Torna alla home', []),
|
||||||
|
|
||||||
|
// --- Promo ed eventi (promo-eventi.astro) ---
|
||||||
|
// Hero
|
||||||
|
T('promo.hero.badge', 'insanity.lab · bologna savena', []),
|
||||||
|
T('promo.hero.title1', 'Allenati con metodo.'),
|
||||||
|
T('promo.hero.accent', 'Scegli come'),
|
||||||
|
T('promo.hero.title2', 'vuoi farlo.'),
|
||||||
|
H('promo.hero.desc', 'Allenati con flessibilità, senza vincoli, da subito.<br>Acquisto online in pochi secondi, pagamento sicuro via Stripe.'),
|
||||||
|
T('promo.hero.cta', 'LE NOSTRE PROPOSTE →', []),
|
||||||
|
// Launch
|
||||||
|
T('promo.launch.badge', 'solo per apertura nuova sede Settembre 2026', []),
|
||||||
|
T('promo.launch.title', 'Il nostro metodo nasce per durare.'),
|
||||||
|
T('promo.launch.accent', "Per l'apertura, un'eccezione. Solo per poco."),
|
||||||
|
T('promo.launch.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."),
|
||||||
|
// Perché 5
|
||||||
|
T('promo.why5.text', 'Non è una prova. È un percorso in miniatura — per capire come lavoriamo prima di impegnarti su un programma trimestrale o semestrale.'),
|
||||||
|
// Label bottone acquisto (condivisa dalle 4 card)
|
||||||
|
T('promo.buy-label', 'ACQUISTA →', []),
|
||||||
|
// Card 1 — One to One
|
||||||
|
T('promo.card.1.eyebrow', 'Personal training', []),
|
||||||
|
T('promo.card.1.title', 'One to One'),
|
||||||
|
T('promo.card.1.price', '163 €', []),
|
||||||
|
T('promo.card.1.price-old', '300 €', []),
|
||||||
|
T('promo.card.1.unit', '5 incontri', []),
|
||||||
|
T('promo.card.1.feature.1', 'Screening iniziale, definizione dei tuoi obiettivi'),
|
||||||
|
T('promo.card.1.feature.2', 'Check-up con profilazione fisica, il tuo punto di partenza'),
|
||||||
|
T('promo.card.1.feature.3', 'PT dedicato, presente a ogni sessione'),
|
||||||
|
T('promo.card.1.feature.4', 'In-sanity plan, il tuo programma personalizzato'),
|
||||||
|
// Card 2 — Small Group (in evidenza)
|
||||||
|
T('promo.card.2.badge', 'Più scelto', []),
|
||||||
|
T('promo.card.2.eyebrow', 'Personal training', []),
|
||||||
|
T('promo.card.2.title', 'Small Group'),
|
||||||
|
T('promo.card.2.price', '110 €', []),
|
||||||
|
T('promo.card.2.price-old', '220 €', []),
|
||||||
|
T('promo.card.2.unit', '5 incontri', []),
|
||||||
|
T('promo.card.2.feature.1', 'Screening iniziale, definizione dei tuoi obiettivi'),
|
||||||
|
T('promo.card.2.feature.2', 'Check-up con profilazione fisica, il tuo punto di partenza'),
|
||||||
|
T('promo.card.2.feature.3', 'PT dedicato, presente a ogni sessione'),
|
||||||
|
T('promo.card.2.feature.4', 'In-sanity plan, il tuo programma personalizzato'),
|
||||||
|
// Card 3 — Reformer
|
||||||
|
T('promo.card.3.eyebrow', 'Pilates class', []),
|
||||||
|
T('promo.card.3.title', 'Reformer'),
|
||||||
|
T('promo.card.3.price', '100 €', []),
|
||||||
|
T('promo.card.3.price-old', '140 €', []),
|
||||||
|
T('promo.card.3.unit', '5 incontri', []),
|
||||||
|
T('promo.card.3.feature.1', 'Core, postura e mobilità su macchina Reformer'),
|
||||||
|
T('promo.card.3.feature.2', 'Lavoro a basso impatto sulle articolazioni'),
|
||||||
|
T('promo.card.3.feature.3', 'Percorso breve, non lezioni isolate'),
|
||||||
|
// Card 4 — Holistic class
|
||||||
|
T('promo.card.4.eyebrow', 'Holistic class', []),
|
||||||
|
T('promo.card.4.title', 'Pilates Mat, Mobility, Yoga'),
|
||||||
|
T('promo.card.4.price', '50 €', []),
|
||||||
|
T('promo.card.4.price-old', '100 €', []),
|
||||||
|
T('promo.card.4.unit', '5 incontri', []),
|
||||||
|
T('promo.card.4.feature.1', 'Mobilità articolare, respirazione, equilibrio'),
|
||||||
|
T('promo.card.4.feature.2', 'Da solo, o integrato nei tuoi allenamenti'),
|
||||||
|
T('promo.card.4.feature.3', 'Adatto a tutti i livelli'),
|
||||||
|
// Nota validità
|
||||||
|
T('promo.validity-note', 'Disponibile solo per l\'apertura · Valido entro 2 mesi dal primo incontro e comunque non oltre il 1° dicembre 2026', []),
|
||||||
|
// FAQ — le risposte usano il segnaposto {{whatsapp}}, sostituito a runtime con global.whatsapp
|
||||||
|
T('promo.faq.title', 'Tutto quello che vuoi sapere prima di acquistare.'),
|
||||||
|
T('promo.faq.1.q', 'Come funziona il pacchetto a incontri per One to One e Small Group?'),
|
||||||
|
H('promo.faq.1.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 ({{whatsapp}})</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.'),
|
||||||
|
T('promo.faq.2.q', 'Come funziona il percorso Reformer o Holistic Class?'),
|
||||||
|
H('promo.faq.2.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 ({{whatsapp}})</span>! Gli incontri sono validi 2 mesi dalla data del primo incontro e comunque non oltre il 1° dicembre 2026.'),
|
||||||
|
T('promo.faq.3.q', 'Posso usare gli incontri su servizi diversi?'),
|
||||||
|
H('promo.faq.3.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.'),
|
||||||
|
T('promo.faq.4.q', 'Posso regalare un pacchetto a qualcun altro?'),
|
||||||
|
H('promo.faq.4.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 ({{whatsapp}})</span> per organizzare le sessioni.'),
|
||||||
|
T('promo.faq.5.q', 'Il pagamento è sicuro?'),
|
||||||
|
H('promo.faq.5.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.'),
|
||||||
|
T('promo.faq.6.q', 'Posso chiedere un rimborso?'),
|
||||||
|
H('promo.faq.6.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.'),
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const seedByTag = new Map(contentSeed.map((e) => [e.tag, e]));
|
export const seedByTag = new Map(contentSeed.map((e) => [e.tag, e]));
|
||||||
|
|||||||
+5
-6
@@ -5,20 +5,19 @@ export const site = {
|
|||||||
// Due ingressi dello stesso stabile. address = testo mostrato; mapQuery = punto geolocalizzabile per Google Maps.
|
// Due ingressi dello stesso stabile. address = testo mostrato; mapQuery = punto geolocalizzabile per Google Maps.
|
||||||
address: "Via Leandro Alberti 76 e Via De Mattiolo 5",
|
address: "Via Leandro Alberti 76 e Via De Mattiolo 5",
|
||||||
cityLine: "40139 Bologna",
|
cityLine: "40139 Bologna",
|
||||||
mapQuery: "Via Leandro Alberti 76, 40139 Bologna",
|
mapQuery: "IN-SANITY LAB, Via Leandro Alberti 76, 40139 Bologna",
|
||||||
hours: ["Lun – Ven 6.00 – 21.00", "Sab 8.00 – 19.00 · Dom chiuso"],
|
hours: ["Lun – Ven 6.00 – 21.00", "Sab 8.00 – 19.00 · Dom chiuso"],
|
||||||
socials: [
|
socials: [
|
||||||
{ label: "Instagram", url: "https://www.instagram.com/insanitylab" },
|
{ label: "Instagram", url: "https://www.instagram.com/in_sanity_lab" },
|
||||||
{ label: "Facebook", url: "https://www.facebook.com/insanitylab" },
|
{ label: "LinkedIn", url: "https://www.linkedin.com/company/in-sanity-lab-srl/" },
|
||||||
{ label: "LinkedIn", url: "https://www.linkedin.com/company/insanitylab" },
|
|
||||||
],
|
],
|
||||||
navigation: [
|
navigation: [
|
||||||
{ label: "About us", href: "/about" },
|
|
||||||
{ label: "Training", href: "/training" },
|
{ label: "Training", href: "/training" },
|
||||||
{ label: "Servizi", href: "/services" },
|
{ label: "Servizi", href: "/services" },
|
||||||
|
{ label: "About us", href: "/about" },
|
||||||
{ label: "Blog & Edugo", href: "/blog" },
|
{ label: "Blog & Edugo", href: "/blog" },
|
||||||
{ label: "Contatti", href: "/contact" },
|
|
||||||
{ label: "Promo ed eventi", href: "/promo-eventi" },
|
{ label: "Promo ed eventi", href: "/promo-eventi" },
|
||||||
|
{ label: "Contatti", href: "/contact" },
|
||||||
],
|
],
|
||||||
footerTraining: [
|
footerTraining: [
|
||||||
{ label: "One to one", href: "/training#one-to-one" },
|
{ label: "One to one", href: "/training#one-to-one" },
|
||||||
|
|||||||
+153
-33
@@ -19,10 +19,17 @@ const user = tok ? getSessionUser(getDb(), tok) : null;
|
|||||||
const canTags = !!user && (user.role === 'superuser' || user.role === 'admin');
|
const canTags = !!user && (user.role === 'superuser' || user.role === 'admin');
|
||||||
const wantTags = Astro.cookies.get('showtags')?.value === '1' || Astro.url.searchParams.get('annotate') === '1';
|
const wantTags = Astro.cookies.get('showtags')?.value === '1' || Astro.url.searchParams.get('annotate') === '1';
|
||||||
const showTags = canTags && wantTags;
|
const showTags = canTags && wantTags;
|
||||||
|
|
||||||
|
// Google Tag Manager: l'ID container arriva da GTM_ID (.env), letto a runtime via process.env
|
||||||
|
// perché in SSR le PUBLIC_* di Vite verrebbero inlinate a build-time. Se vuoto GTM non carica.
|
||||||
|
const gtmId = process.env.GTM_ID;
|
||||||
---
|
---
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="it">
|
<html lang="it">
|
||||||
<head>
|
<head>
|
||||||
|
{gtmId && (
|
||||||
|
<script is:inline set:html={`(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','${gtmId}');`} />
|
||||||
|
)}
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>{title} — InsanityLab</title>
|
<title>{title} — InsanityLab</title>
|
||||||
@@ -36,6 +43,9 @@ const showTags = canTags && wantTags;
|
|||||||
<script is:inline>document.documentElement.classList.add('js');</script>
|
<script is:inline>document.documentElement.classList.add('js');</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{gtmId && (
|
||||||
|
<noscript set:html={`<iframe src="https://www.googletagmanager.com/ns.html?id=${gtmId}" height="0" width="0" style="display:none;visibility:hidden"></iframe>`} />
|
||||||
|
)}
|
||||||
<Header user={user} showTags={showTags} canTags={canTags} />
|
<Header user={user} showTags={showTags} canTags={canTags} />
|
||||||
<main><slot /></main>
|
<main><slot /></main>
|
||||||
<Footer />
|
<Footer />
|
||||||
@@ -77,37 +87,108 @@ const showTags = canTags && wantTags;
|
|||||||
<style is:global>
|
<style is:global>
|
||||||
[data-tag] { outline: 1px dashed #b8a98c; outline-offset: 2px; }
|
[data-tag] { outline: 1px dashed #b8a98c; outline-offset: 2px; }
|
||||||
.tag-badge { position: absolute; z-index: 9999; display: inline-flex; align-items: center; gap: 4px;
|
.tag-badge { position: absolute; z-index: 9999; display: inline-flex; align-items: center; gap: 4px;
|
||||||
background: #3a2929; color: #fff; font: 600 10px/1.6 monospace; padding: 0 4px 0 6px; opacity: .85; }
|
background: #3a2929; color: #fff; font: 600 10px/1.6 monospace; padding: 0 4px 0 6px; opacity: .85; border-radius: 3px; }
|
||||||
.tag-badge:hover { opacity: 1; }
|
.tag-badge:hover { opacity: 1; }
|
||||||
.tag-badge__edit { background: #9c8b70; color: #fff; border: 0; cursor: pointer; font: inherit; padding: 0 5px; }
|
.tag-badge__edit { background: #9c8b70; color: #fff; border: 0; cursor: pointer; font: inherit; padding: 0 5px; border-radius: 2px; }
|
||||||
.tag-modal { position: fixed; inset: 0; z-index: 10000; background: rgba(0,0,0,.5); display: flex; align-items: center; justify-content: center; }
|
.tag-modal { position: fixed; inset: 0; z-index: 10000; background: rgba(28,20,16,.55); backdrop-filter: blur(2px);
|
||||||
|
display: flex; align-items: center; justify-content: center; padding: 20px; }
|
||||||
.tag-modal[hidden] { display: none; }
|
.tag-modal[hidden] { display: none; }
|
||||||
.tag-modal__box { background: #fff; color: #222; width: min(560px, 92vw); max-height: 88vh; overflow: auto; padding: 20px; border-radius: 4px; }
|
.tag-modal__box { background: #fdfbf7; color: #2b2320; width: min(600px, 94vw); max-height: 88vh; overflow: auto;
|
||||||
.tag-modal__box h3 { margin: 0 0 4px; font: 600 14px monospace; }
|
border-radius: 14px; box-shadow: 0 24px 60px rgba(28,20,16,.35); font-family: system-ui, sans-serif; }
|
||||||
.tag-modal__box .guid { font: 11px monospace; color: #888; margin: 0 0 14px; }
|
.tag-modal__head { padding: 20px 24px 16px; border-bottom: 1px solid #ece4d8; }
|
||||||
.tag-modal__box textarea { width: 100%; min-height: 140px; font: inherit; padding: 8px; box-sizing: border-box; }
|
.tag-modal__head h3 { margin: 0; font: 600 1.05rem/1.3 system-ui, sans-serif; letter-spacing: .01em; }
|
||||||
.tag-modal__box select { margin: 6px 8px 6px 0; }
|
.tag-modal__head .tm-tagchip { display: inline-block; margin-top: 8px; background: #efe8dc; color: #6b5b48;
|
||||||
.tag-modal__row { display: flex; gap: 10px; margin-top: 14px; }
|
font: 500 11px/1.6 ui-monospace, monospace; padding: 2px 9px; border-radius: 999px; word-break: break-all; }
|
||||||
.tag-modal__row button { padding: 9px 16px; border: 0; cursor: pointer; }
|
.tag-modal__head .guid { font: 10px/1.5 ui-monospace, monospace; color: #a99f90; margin: 6px 0 0; }
|
||||||
|
.tag-modal__body { padding: 20px 24px; }
|
||||||
|
.tag-modal__box textarea { width: 100%; min-height: 150px; font: inherit; font-size: .95rem; line-height: 1.5;
|
||||||
|
padding: 12px 14px; box-sizing: border-box; border: 1px solid #dcd2c4; border-radius: 10px; background: #fff;
|
||||||
|
resize: vertical; color: #2b2320; }
|
||||||
|
.tag-modal__box textarea:focus { outline: 2px solid #9c8b70; outline-offset: 1px; border-color: #9c8b70; }
|
||||||
|
.tm-styles { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px 14px; margin-top: 18px; }
|
||||||
|
.tm-field { display: flex; flex-direction: column; gap: 5px; }
|
||||||
|
.tm-field__label { font: 600 11px/1 system-ui, sans-serif; text-transform: uppercase; letter-spacing: .08em; color: #8a7c68; }
|
||||||
|
.tm-field select { appearance: none; width: 100%; padding: 9px 30px 9px 12px; border: 1px solid #dcd2c4; border-radius: 9px;
|
||||||
|
background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%238a7c68' stroke-width='1.6'%3E%3Cpath d='M2 4l4 4 4-4'/%3E%3C/svg%3E") no-repeat right 11px center;
|
||||||
|
font: inherit; font-size: .9rem; color: #2b2320; cursor: pointer; }
|
||||||
|
.tm-field select:focus { outline: 2px solid #9c8b70; outline-offset: 1px; border-color: #9c8b70; }
|
||||||
|
.tm-field select.is-set { border-color: #9c8b70; background-color: #f6f1e8; font-weight: 600; }
|
||||||
|
.tm-field select.is-estimated { border-style: dashed; border-color: #c4b79f; color: #6b5b48; }
|
||||||
|
.tm-note { margin: 12px 0 0; font: 400 12px/1.5 system-ui, sans-serif; color: #8a7c68; }
|
||||||
|
.tag-modal__box input[type=file] { display: block; width: 100%; font: inherit; font-size: .9rem; padding: 10px;
|
||||||
|
border: 1px dashed #cbbfad; border-radius: 10px; background: #fff; box-sizing: border-box; }
|
||||||
|
.tm-del { margin-top: 12px; padding: 8px 14px; border: 0; border-radius: 8px; background: #b4443a; color: #fff; cursor: pointer; font: 600 .85rem system-ui, sans-serif; }
|
||||||
|
.tag-modal__msg { color: #b4443a; margin: 12px 24px 0; min-height: 18px; font-size: .88rem; }
|
||||||
|
.tag-modal__row { display: flex; gap: 10px; justify-content: flex-end; padding: 16px 24px 22px; }
|
||||||
|
.tag-modal__row button { padding: 10px 20px; border: 0; border-radius: 9px; cursor: pointer; font: 600 .9rem system-ui, sans-serif; }
|
||||||
.tag-modal__save { background: #3a2d26; color: #fff; }
|
.tag-modal__save { background: #3a2d26; color: #fff; }
|
||||||
.tag-modal__cancel { background: #ddd; }
|
.tag-modal__save:hover { background: #2b211b; }
|
||||||
.tag-modal__msg { color: #a33; margin-top: 8px; min-height: 18px; }
|
.tag-modal__cancel { background: #ece4d8; color: #5b4f42; }
|
||||||
|
.tag-modal__cancel:hover { background: #e0d6c6; }
|
||||||
</style>
|
</style>
|
||||||
<div class="tag-modal" id="tag-modal" hidden>
|
<div class="tag-modal" id="tag-modal" hidden>
|
||||||
<div class="tag-modal__box">
|
<div class="tag-modal__box">
|
||||||
<h3 id="tm-tag"></h3>
|
<div class="tag-modal__head">
|
||||||
<p class="guid" id="tm-guid"></p>
|
<h3>Modifica contenuto</h3>
|
||||||
<div id="tm-body"></div>
|
<span class="tm-tagchip" id="tm-tag"></span>
|
||||||
|
<p class="guid" id="tm-guid"></p>
|
||||||
|
</div>
|
||||||
|
<div class="tag-modal__body" id="tm-body"></div>
|
||||||
<div class="tag-modal__msg" id="tm-msg"></div>
|
<div class="tag-modal__msg" id="tm-msg"></div>
|
||||||
<div class="tag-modal__row">
|
<div class="tag-modal__row">
|
||||||
<button type="button" class="tag-modal__save" id="tm-save">Salva</button>
|
|
||||||
<button type="button" class="tag-modal__cancel" id="tm-cancel">Annulla</button>
|
<button type="button" class="tag-modal__cancel" id="tm-cancel">Annulla</button>
|
||||||
|
<button type="button" class="tag-modal__save" id="tm-save">Salva</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
(() => {
|
(() => {
|
||||||
const STYLE_GROUPS = { size: ['s','m','l'], color: ['accent','accent-dark','dark','heading','text','text-light','white'], weight: ['normal','bold'], style: ['normal','italic'], align: ['left','center','right'] };
|
const STYLE_GROUPS = { size: ['s','m','l','xl','xxl'], color: ['accent','accent-dark','dark','heading','text','text-light','white'], weight: ['normal','bold'], style: ['normal','italic'], align: ['left','center','right','justify'] };
|
||||||
|
const GROUP_LABELS = { size: 'Dimensione', color: 'Colore', weight: 'Peso', style: 'Stile', align: 'Allineamento' };
|
||||||
|
const VALUE_LABELS = {
|
||||||
|
size: { s: 'S', m: 'M', l: 'L', xl: 'XL', xxl: 'XXL' },
|
||||||
|
weight: { normal: 'Normale', bold: 'Grassetto' },
|
||||||
|
style: { normal: 'Normale', italic: 'Corsivo' },
|
||||||
|
align: { left: 'Sinistra', center: 'Centro', right: 'Destra', justify: 'Giustificato' },
|
||||||
|
color: { accent: 'Accento', 'accent-dark': 'Accento scuro', dark: 'Scuro', heading: 'Titolo', text: 'Testo', 'text-light': 'Testo chiaro', white: 'Bianco' },
|
||||||
|
};
|
||||||
|
const valLabel = (g, v) => (VALUE_LABELS[g] && VALUE_LABELS[g][v]) || v;
|
||||||
|
|
||||||
|
// Stima il preset più vicino all'aspetto REALE dell'elemento (getComputedStyle).
|
||||||
|
const px = (s) => parseFloat(s) || 0;
|
||||||
|
const rgb = (s) => { const m = String(s).match(/-?\d+(\.\d+)?/g); return m ? m.slice(0, 3).map(Number) : null; };
|
||||||
|
const SIZE_SCALE = { s: 0.85, m: 1, l: 1.25, xl: 1.5, xxl: 2 };
|
||||||
|
let colorProbe = null;
|
||||||
|
function nearestSize(el) {
|
||||||
|
const cs = getComputedStyle(el);
|
||||||
|
const ref = el.parentElement ? px(getComputedStyle(el.parentElement).fontSize) : px(cs.fontSize);
|
||||||
|
const ratio = ref ? px(cs.fontSize) / ref : 1;
|
||||||
|
let best = 'm', bd = Infinity;
|
||||||
|
for (const k in SIZE_SCALE) { const d = Math.abs(SIZE_SCALE[k] - ratio); if (d < bd) { bd = d; best = k; } }
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
function nearestColor(el) {
|
||||||
|
const a = rgb(getComputedStyle(el).color); if (!a) return null;
|
||||||
|
if (!colorProbe) { colorProbe = document.createElement('span'); colorProbe.style.cssText = 'position:absolute;left:-9999px;visibility:hidden'; document.body.appendChild(colorProbe); }
|
||||||
|
let best = null, bd = Infinity;
|
||||||
|
STYLE_GROUPS.color.forEach((c) => {
|
||||||
|
colorProbe.className = 'cs-color-' + c;
|
||||||
|
const p = rgb(getComputedStyle(colorProbe).color); if (!p) return;
|
||||||
|
const d = (p[0]-a[0])**2 + (p[1]-a[1])**2 + (p[2]-a[2])**2;
|
||||||
|
if (d < bd) { bd = d; best = c; }
|
||||||
|
});
|
||||||
|
colorProbe.className = '';
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
function effective(group, el) {
|
||||||
|
const cs = getComputedStyle(el);
|
||||||
|
if (group === 'size') return nearestSize(el);
|
||||||
|
if (group === 'color') return nearestColor(el);
|
||||||
|
if (group === 'weight') return parseInt(cs.fontWeight, 10) >= 600 ? 'bold' : 'normal';
|
||||||
|
if (group === 'style') return cs.fontStyle.indexOf('italic') >= 0 ? 'italic' : 'normal';
|
||||||
|
if (group === 'align') { let x = cs.textAlign; if (x === 'start') x = 'left'; else if (x === 'end') x = 'right'; return ['left','center','right','justify'].indexOf(x) >= 0 ? x : 'left'; }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const modal = document.getElementById('tag-modal');
|
const modal = document.getElementById('tag-modal');
|
||||||
const tmTag = document.getElementById('tm-tag');
|
const tmTag = document.getElementById('tm-tag');
|
||||||
const tmGuid = document.getElementById('tm-guid');
|
const tmGuid = document.getElementById('tm-guid');
|
||||||
@@ -137,8 +218,7 @@ const showTags = canTags && wantTags;
|
|||||||
inp.type = 'file'; inp.accept = 'image/*'; inp.id = 'tm-file';
|
inp.type = 'file'; inp.accept = 'image/*'; inp.id = 'tm-file';
|
||||||
tmBody.appendChild(inp);
|
tmBody.appendChild(inp);
|
||||||
const del = document.createElement('button');
|
const del = document.createElement('button');
|
||||||
del.type = 'button'; del.textContent = 'Rimuovi immagine'; del.id = 'tm-del';
|
del.type = 'button'; del.textContent = 'Rimuovi immagine'; del.id = 'tm-del'; del.className = 'tm-del';
|
||||||
del.style.cssText = 'margin-top:10px;padding:8px 14px;border:0;background:#a33;color:#fff;cursor:pointer';
|
|
||||||
tmBody.appendChild(del);
|
tmBody.appendChild(del);
|
||||||
del.addEventListener('click', async () => {
|
del.addEventListener('click', async () => {
|
||||||
const r = await fetch('/api/admin/content/' + encodeURIComponent(current.tag) + '/image', { method: 'DELETE' });
|
const r = await fetch('/api/admin/content/' + encodeURIComponent(current.tag) + '/image', { method: 'DELETE' });
|
||||||
@@ -148,18 +228,52 @@ const showTags = canTags && wantTags;
|
|||||||
const ta = document.createElement('textarea');
|
const ta = document.createElement('textarea');
|
||||||
ta.id = 'tm-value'; ta.value = data.value;
|
ta.id = 'tm-value'; ta.value = data.value;
|
||||||
tmBody.appendChild(ta);
|
tmBody.appendChild(ta);
|
||||||
(data.styleOptions || []).forEach((group) => {
|
const styles = data.styles || {};
|
||||||
const opts = STYLE_GROUPS[group];
|
const groups = (data.styleOptions || []).filter((g) => STYLE_GROUPS[g]);
|
||||||
if (!opts) return;
|
if (groups.length) {
|
||||||
const sel = document.createElement('select');
|
const grid = document.createElement('div');
|
||||||
sel.dataset.group = group;
|
grid.className = 'tm-styles';
|
||||||
sel.className = 'tm-style';
|
let anyEstimate = false;
|
||||||
const none = document.createElement('option');
|
groups.forEach((group) => {
|
||||||
none.value = ''; none.textContent = group + ': —';
|
const explicit = styles[group]; // valore salvato nel blocco
|
||||||
sel.appendChild(none);
|
const est = explicit ? null : effective(group, el); // altrimenti stima dall'aspetto reale
|
||||||
opts.forEach((v) => { const o = document.createElement('option'); o.value = v; o.textContent = group + ': ' + v; sel.appendChild(o); });
|
const shown = explicit || est;
|
||||||
tmBody.appendChild(sel);
|
if (est) anyEstimate = true;
|
||||||
});
|
const field = document.createElement('label');
|
||||||
|
field.className = 'tm-field';
|
||||||
|
const cap = document.createElement('span');
|
||||||
|
cap.className = 'tm-field__label';
|
||||||
|
cap.textContent = GROUP_LABELS[group] || group;
|
||||||
|
const sel = document.createElement('select');
|
||||||
|
sel.dataset.group = group;
|
||||||
|
sel.className = 'tm-style';
|
||||||
|
if (est) sel.dataset.estimated = est; // marcata come stima: non fissata al salvataggio se invariata
|
||||||
|
const none = document.createElement('option');
|
||||||
|
none.value = ''; none.textContent = '— default (tema) —';
|
||||||
|
sel.appendChild(none);
|
||||||
|
STYLE_GROUPS[group].forEach((v) => {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = v; o.textContent = valLabel(group, v) + (est === v ? ' (attuale)' : '');
|
||||||
|
if (shown === v) o.selected = true;
|
||||||
|
sel.appendChild(o);
|
||||||
|
});
|
||||||
|
sel.classList.add(explicit ? 'is-set' : (est ? 'is-estimated' : ''));
|
||||||
|
sel.addEventListener('change', () => {
|
||||||
|
delete sel.dataset.estimated; // scelta manuale → diventa esplicita
|
||||||
|
sel.classList.remove('is-estimated');
|
||||||
|
sel.classList.toggle('is-set', !!sel.value);
|
||||||
|
});
|
||||||
|
field.appendChild(cap); field.appendChild(sel);
|
||||||
|
grid.appendChild(field);
|
||||||
|
});
|
||||||
|
tmBody.appendChild(grid);
|
||||||
|
if (anyEstimate) {
|
||||||
|
const note = document.createElement('p');
|
||||||
|
note.className = 'tm-note';
|
||||||
|
note.textContent = 'I valori "(attuale)" riflettono l’aspetto dal tema: cambiane uno per fissarlo sul blocco.';
|
||||||
|
tmBody.appendChild(note);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
modal.hidden = false;
|
modal.hidden = false;
|
||||||
}
|
}
|
||||||
@@ -176,10 +290,16 @@ const showTags = canTags && wantTags;
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const value = document.getElementById('tm-value').value;
|
const value = document.getElementById('tm-value').value;
|
||||||
|
const selects = document.querySelectorAll('.tm-style');
|
||||||
const styles = {};
|
const styles = {};
|
||||||
document.querySelectorAll('.tm-style').forEach((s) => { if (s.value) styles[s.dataset.group] = s.value; });
|
selects.forEach((s) => {
|
||||||
|
const v = s.value;
|
||||||
|
if (!v) return; // "default (tema)" → non impostato
|
||||||
|
if (s.dataset.estimated && v === s.dataset.estimated) return; // stima non modificata → non fissare
|
||||||
|
styles[s.dataset.group] = v;
|
||||||
|
});
|
||||||
const body = { value };
|
const body = { value };
|
||||||
if (Object.keys(styles).length) body.styles = styles;
|
if (selects.length) body.styles = styles; // inviato sempre (anche {}) così l'azzeramento persiste
|
||||||
const r = await fetch('/api/admin/content/' + encodeURIComponent(current.tag), {
|
const r = await fetch('/api/admin/content/' + encodeURIComponent(current.tag), {
|
||||||
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
|
method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
export const STYLE_GROUPS = {
|
export const STYLE_GROUPS = {
|
||||||
size: ['s', 'm', 'l'],
|
size: ['s', 'm', 'l', 'xl', 'xxl'],
|
||||||
color: ['accent', 'accent-dark', 'dark', 'heading', 'text', 'text-light', 'white'],
|
color: ['accent', 'accent-dark', 'dark', 'heading', 'text', 'text-light', 'white'],
|
||||||
weight: ['normal', 'bold'],
|
weight: ['normal', 'bold'],
|
||||||
style: ['normal', 'italic'],
|
style: ['normal', 'italic'],
|
||||||
align: ['left', 'center', 'right'],
|
align: ['left', 'center', 'right', 'justify'],
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type StyleGroup = keyof typeof STYLE_GROUPS;
|
export type StyleGroup = keyof typeof STYLE_GROUPS;
|
||||||
|
|||||||
@@ -10,14 +10,20 @@ const json = (status: number, body: object) =>
|
|||||||
|
|
||||||
export const GET: APIRoute = ({ params }) => {
|
export const GET: APIRoute = ({ params }) => {
|
||||||
const tag = params.tag ?? '';
|
const tag = params.tag ?? '';
|
||||||
const row = getDb().prepare('SELECT tag, type, value, guid FROM content_blocks WHERE tag = ?').get(tag) as
|
const row = getDb().prepare('SELECT tag, type, value, styles, guid FROM content_blocks WHERE tag = ?').get(tag) as
|
||||||
{ tag: string; type: string; value: string; guid: string | null } | undefined;
|
{ tag: string; type: string; value: string; styles: string; guid: string | null } | undefined;
|
||||||
if (!row) return json(404, { error: 'Tag inesistente.' });
|
if (!row) return json(404, { error: 'Tag inesistente.' });
|
||||||
|
let styles: Record<string, string> = {};
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(row.styles);
|
||||||
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) styles = parsed;
|
||||||
|
} catch { /* styles corrotto: si ignora */ }
|
||||||
return json(200, {
|
return json(200, {
|
||||||
tag: row.tag,
|
tag: row.tag,
|
||||||
guid: row.guid ?? '',
|
guid: row.guid ?? '',
|
||||||
type: row.type,
|
type: row.type,
|
||||||
value: row.value,
|
value: row.value,
|
||||||
|
styles,
|
||||||
styleOptions: seedByTag.get(row.tag)?.styleOptions ?? [],
|
styleOptions: seedByTag.get(row.tag)?.styleOptions ?? [],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
+54
-110
@@ -1,142 +1,86 @@
|
|||||||
---
|
---
|
||||||
import Base from '../layouts/Base.astro';
|
import Base from '../layouts/Base.astro';
|
||||||
|
import T from '../components/content/T.astro';
|
||||||
|
import { t, resolveContent } from '../lib/content';
|
||||||
// Landing promo/lancio integrata nel sito (header e footer del sito, non standalone).
|
// 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,
|
// 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:
|
// 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.
|
// 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';
|
const BSPORT_URL = 'https://backoffice.bsport.io/login/signup?membership=3428';
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
|
// I testi sono editabili dal pannello contenuti (tag `promo.*`). Qui resta solo la
|
||||||
|
// struttura: quante card, quante feature ciascuna e quale è "in evidenza".
|
||||||
|
const cards = [
|
||||||
|
{ id: 1, features: 4, featured: false },
|
||||||
|
{ id: 2, features: 4, featured: true },
|
||||||
|
{ id: 3, features: 3, featured: false },
|
||||||
|
{ id: 4, features: 3, featured: false },
|
||||||
|
];
|
||||||
|
const faqs = [1, 2, 3, 4, 5, 6];
|
||||||
|
// Numero WhatsApp centralizzato: le risposte FAQ contengono il segnaposto {{whatsapp}}
|
||||||
|
// che qui viene sostituito col valore del tag globale, così si aggiorna in un punto solo.
|
||||||
|
const wa = t('global.whatsapp');
|
||||||
---
|
---
|
||||||
<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.">
|
<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="lp">
|
||||||
<div class="page">
|
<div class="page">
|
||||||
|
|
||||||
<section class="hero">
|
<section class="hero">
|
||||||
<div class="badge">insanity.lab · bologna savena</div>
|
<T tag="promo.hero.badge" as="div" class="badge" />
|
||||||
<h1>Allenati con metodo.</h1>
|
<T tag="promo.hero.title1" as="h1" />
|
||||||
<p class="accent">Scegli come</p>
|
<T tag="promo.hero.accent" as="p" class="accent" />
|
||||||
<h1>vuoi farlo.</h1>
|
<T tag="promo.hero.title2" as="h1" />
|
||||||
<p class="desc">Allenati con flessibilità, senza vincoli, da subito.<br>Acquisto online in pochi secondi, pagamento sicuro via Stripe.</p>
|
<T tag="promo.hero.desc" as="p" class="desc" />
|
||||||
<a class="btn-sand" href="#proposte">LE NOSTRE PROPOSTE →</a>
|
<a class="btn-sand" href="#proposte"><T tag="promo.hero.cta" as="span" /></a>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="launch">
|
<section class="launch">
|
||||||
<div class="badge">solo per apertura nuova sede Settembre 2026</div>
|
<T tag="promo.launch.badge" as="div" class="badge" />
|
||||||
<h1>Il nostro metodo nasce per durare.</h1>
|
<T tag="promo.launch.title" as="h1" />
|
||||||
<p class="accent">Per l'apertura, un'eccezione. Solo per poco.</p>
|
<T tag="promo.launch.accent" as="p" class="accent" />
|
||||||
<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>
|
<T tag="promo.launch.desc" as="p" class="desc" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="why5">
|
<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>
|
<T tag="promo.why5.text" as="p" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="cards-wrap" id="proposte">
|
<section class="cards-wrap" id="proposte">
|
||||||
<div class="cards">
|
<div class="cards">
|
||||||
|
{cards.map((c) => (
|
||||||
<div class="card">
|
<div class:list={['card', c.featured && 'featured']}>
|
||||||
<div class="eyebrow">Personal training</div>
|
{c.featured && <T tag={`promo.card.${c.id}.badge`} as="div" class="featured-badge" />}
|
||||||
<div class="title">One to One</div>
|
<T tag={`promo.card.${c.id}.eyebrow`} as="div" class="eyebrow" />
|
||||||
<div class="price-row">
|
<T tag={`promo.card.${c.id}.title`} as="div" class="title" />
|
||||||
<div class="price">163 €</div>
|
<div class="price-row">
|
||||||
<div class="price-old">300 €</div>
|
<T tag={`promo.card.${c.id}.price`} as="div" class="price" />
|
||||||
|
<T tag={`promo.card.${c.id}.price-old`} as="div" class="price-old" />
|
||||||
|
</div>
|
||||||
|
<T tag={`promo.card.${c.id}.unit`} as="div" class="unit" />
|
||||||
|
<ul>
|
||||||
|
{Array.from({ length: c.features }, (_, i) => i + 1).map((m) => (
|
||||||
|
<T tag={`promo.card.${c.id}.feature.${m}`} as="li" />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<a class="buy" href={BSPORT_URL} target="_blank" rel="noopener"><T tag="promo.buy-label" as="span" /></a>
|
||||||
</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>
|
</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>
|
<T tag="promo.validity-note" as="p" class="validity-note" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="faq-wrap">
|
<section class="faq-wrap">
|
||||||
<h1>Tutto quello che vuoi sapere prima di acquistare.</h1>
|
<T tag="promo.faq.title" as="h1" />
|
||||||
|
{faqs.map((n) => {
|
||||||
<div class="faq-block">
|
const a = resolveContent(`promo.faq.${n}.a`);
|
||||||
<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>
|
return (
|
||||||
<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 class="faq-block">
|
||||||
</div>
|
<div class="faq-q"><T tag={`promo.faq.${n}.q`} as="p" /><div class="faq-toggle">+</div></div>
|
||||||
|
<p class:list={['faq-a', a.classes]} data-tag={`promo.faq.${n}.a`} data-guid={a.guid || undefined} set:html={a.value.replaceAll('{{whatsapp}}', wa)} />
|
||||||
<div class="faq-block">
|
</div>
|
||||||
<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>
|
</section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ html.js [data-reveal].is-inview, html.js .section-heading.is-inview { opacity: 1
|
|||||||
.cs-size-s { font-size: .85em; }
|
.cs-size-s { font-size: .85em; }
|
||||||
.cs-size-m { font-size: 1em; }
|
.cs-size-m { font-size: 1em; }
|
||||||
.cs-size-l { font-size: 1.25em; }
|
.cs-size-l { font-size: 1.25em; }
|
||||||
|
.cs-size-xl { font-size: 1.5em; }
|
||||||
|
.cs-size-xxl { font-size: 2em; }
|
||||||
.cs-color-accent { color: var(--c-accent); }
|
.cs-color-accent { color: var(--c-accent); }
|
||||||
.cs-color-accent-dark { color: var(--c-accent-dark); }
|
.cs-color-accent-dark { color: var(--c-accent-dark); }
|
||||||
.cs-color-dark { color: var(--c-dark); }
|
.cs-color-dark { color: var(--c-dark); }
|
||||||
@@ -87,3 +89,4 @@ html.js [data-reveal].is-inview, html.js .section-heading.is-inview { opacity: 1
|
|||||||
.cs-align-left { text-align: left; }
|
.cs-align-left { text-align: left; }
|
||||||
.cs-align-center { text-align: center; }
|
.cs-align-center { text-align: center; }
|
||||||
.cs-align-right { text-align: right; }
|
.cs-align-right { text-align: right; }
|
||||||
|
.cs-align-justify { text-align: justify; }
|
||||||
|
|||||||
Reference in New Issue
Block a user