Compare commits

..

4 Commits

Author SHA1 Message Date
Adriano 708410bbf1 feat(home): rimuove le sezioni Agenda e Team dalla pagina principale
Tolte da index.astro le sezioni <Agenda> e <TeamSection> con i relativi import.
I componenti restano nel codice (riutilizzabili altrove). Modifica isolata su
branch dedicato per valutazione col cliente.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:41:47 +02:00
Adriano 53af0ca00e feat(home): nuova immagine per lo step "Insanity plan" del Metodo
Grafica di brand (barre di progressione, curva, checklist protocolli) al posto
della vecchia foto, coerente col testo aggiornato dello step 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:40:57 +02:00
Adriano 6594f6da53 docs(env): configurazione SMTP Aruba per il form contatti
smtps.aruba.it:465 con casella autenticata del dominio (il mittente deve
coincidere con la casella SMTP). Il dominio invia già la posta via Aruba
(SPF include:aruba.it). Credenziali reali solo nel .env (git-ignored) e sul VPS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 13:41:20 +02:00
Adriano c6de02be83 Contatti, /training e immagini: rifiniture post-merge
- Rimossa la sezione "Prenota la lezione di prova" da /training (markup,
  import, CSS, seed e blocchi DB relativi)
- Titoli dei riquadri Training non più forzati in minuscolo: rispettano il
  testo del DB (One to One, Small Groups, ecc.)
- Footer e pagina Contatti con i due ingressi dello stesso stabile
  (Via Leandro Alberti 76 e Via De Mattiolo 5) più l'email; nuovo tag
  global.contact.map-query e migrazione DB idempotente dell'indirizzo
- Mappa Google in /contact caricata automaticamente e a piena dimensione
  (rimosso il bottone e il relativo script)
- Landing Promo ed eventi: rimossa la barra contatti in fondo
- service-icons allineate agli slug attuali (one-to-one, small-group;
  rimosse le chiavi obsolete personal-training e pilates-flow)
- Immagini: foto Rehab corretta (prima era di osteopatia) su program-rehab
  e training-rehab; bande nere letterbox rimosse dalla foto di osteopatia;
  nuova immagine grafica per Profilazione con grafici HRV (ECG, tacogramma
  R-R, Poincaré plot)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 12:41:37 +02:00
16 changed files with 29 additions and 57 deletions
+10 -5
View File
@@ -1,8 +1,13 @@
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=user
SMTP_PASS=pass
# SMTP Aruba — il dominio insanitylab.it invia la posta via Aruba (SPF: include:aruba.it)
# Server posta in uscita autenticato: smtps.aruba.it:465 (SSL). Verificato funzionante.
SMTP_HOST=smtps.aruba.it
SMTP_PORT=465
# Casella reale del dominio usata per l'invio autenticato (con la sua password Aruba)
SMTP_USER=postmaster@insanitylab.it
SMTP_PASS=la-password-della-casella
# Aruba impone che il mittente sia la casella autenticata: uguale a SMTP_USER
CONTACT_FROM=postmaster@insanitylab.it
# Dove arrivano i messaggi del form
CONTACT_TO=info@insanitylab.it
CONTACT_FROM=sito@insanitylab.it
DB_PATH=data/insanitylab.db
UPLOADS_DIR=uploads
Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 192 KiB

+1 -1
View File
@@ -11,7 +11,7 @@ const year = new Date().getFullYear();
<img src={contentImageUrl('footer.logo.image', '/img/logo-light.png')} alt="InsanityLab" width="140" loading="lazy" />
<T tag="footer.tagline" as="p" class="ftr__tag" />
<T tag="footer.about" as="p" />
<p>{t('global.contact.phone')}<br />{t('global.contact.address')}, {t('global.contact.city-line')}</p>
<p>{t('global.contact.address')}, {t('global.contact.city-line')}<br />{t('global.contact.phone')}<br /><a href={`mailto:${t('global.contact.email')}`}>{t('global.contact.email')}</a></p>
<p class="ftr__hours">{t('global.hours.1')}</p>
<p class="ftr__hours">{t('global.hours.2')}</p>
</div>
+1 -4
View File
@@ -23,6 +23,7 @@ export const contentSeed: SeedEntry[] = [
T('global.contact.email', site.email, []),
T('global.contact.address', site.address, []),
T('global.contact.city-line', site.cityLine, []),
T('global.contact.map-query', site.mapQuery, []),
T('global.hours.1', site.hours[0], []),
T('global.hours.2', site.hours[1], []),
T('global.nav.1.label', site.navigation[0].label, []),
@@ -278,10 +279,6 @@ export const contentSeed: SeedEntry[] = [
T('training.meta.description', 'One to One, Small Group, Rehab, Holistic Class, Reformer Class e Coaching: trova il training InsanityLab più adatto a te.', []),
T('training.hero.title', 'training', []),
T('training.intro.subtitle', 'Porta i tuoi allenamenti al livello successivo: programmi completi e percorsi su misura. Trova il training più adatto alle tue esigenze.'),
T('training.trial.title', 'Prenota la lezione di prova'),
T('training.trial.body', "La lezione di prova è l'occasione perfetta per conoscere il metodo, vivere l'esperienza in prima persona e capire il percorso più adatto a te. Inizia oggi il tuo percorso: prenota la tua lezione di prova."),
T('training.trial.cta', 'Prenota', []),
I('training.trial.image'),
// --- Contact ---
T('contact.meta.title', 'Contatti', []),
+2 -2
View File
@@ -1,9 +1,9 @@
// Path SVG (stroke, viewBox 0 0 24 24) delle icone servizi, condivisi tra griglia home e sidebar
export const serviceIcons: Record<string, string> = {
'personal-training': 'M4 12h3m10 0h3M9 8v8m6-8v8M7 10v4m10-4v4',
'one-to-one': 'M4 12h3m10 0h3M9 8v8m6-8v8M7 10v4m10-4v4',
'small-group': 'M4 12h3m10 0h3M9 8v8m6-8v8M7 10v4m10-4v4',
'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',
'pilates-flow': 'M12 4a2 2 0 110 4 2 2 0 010-4zM6 20c2-6 10-6 12 0M12 8v6',
'rehab': 'M12 21C7 17 3 13 3 9a5 5 0 019-3 5 5 0 019 3c0 4-4 8-9 12z',
'nutrition': 'M12 3c4 0 7 3 7 8s-3 10-7 10-7-5-7-10 3-8 7-8zm0-1v4',
// Servizi rinominati (modifiche 02): profilazione (grafico/analisi), osteopatia (mani)
+3 -1
View File
@@ -2,8 +2,10 @@ export const site = {
name: "InsanityLab",
phone: "351 7535977",
email: "info@insanitylab.it",
address: "Via Leandro Alberti, 76",
// Due ingressi dello stesso stabile. address = testo mostrato; mapQuery = punto geolocalizzabile per Google Maps.
address: "Via Leandro Alberti 76 e Via De Mattiolo 5",
cityLine: "40139 Bologna",
mapQuery: "Via Leandro Alberti 76, 40139 Bologna",
hours: ["Lun Ven 6.00 21.00", "Sab 8.00 19.00 · Dom chiuso"],
socials: [
{ label: "Instagram", url: "https://www.instagram.com/insanitylab" },
+5
View File
@@ -150,6 +150,11 @@ export function createDb(path?: string): Database.Database {
const upd02Tx = db.transaction(() => { for (const [tag, oldV, newV] of SERVIZI_02) upd02.run(newV, tag, oldV); });
upd02Tx();
// Modifiche 03: due ingressi dello stesso stabile → indirizzo esteso in footer e contatti.
// Il tag global.contact.map-query è nuovo (inserito dal seed); qui si aggiorna solo l'indirizzo
// già esistente, se non modificato dal pannello. Idempotente.
db.prepare("UPDATE content_blocks SET value = 'Via Leandro Alberti 76 e Via De Mattiolo 5' WHERE tag = 'global.contact.address' AND value = 'Via Leandro Alberti, 76'").run();
// Modifiche 02 (pagina Insanitylab/about): testi riscritti. Guardia via frammento LIKE
// distintivo del vecchio testo → robusto agli apostrofi tipografici, idempotente e
// rispettoso delle eventuali riscritture da pannello (se il frammento non c'è più, salta).
+5 -14
View File
@@ -6,15 +6,14 @@ import contactHero from '../assets/img/contact-hero.jpg';
import T from '../components/content/T.astro';
import { t, getImageOverride } from '../lib/content';
export const prerender = false;
const mapsQuery = encodeURIComponent(`${t('global.contact.address')}, ${t('global.contact.city-line')}`);
const mapsQuery = encodeURIComponent(t('global.contact.map-query'));
---
<Base title={t('contact.meta.title')} description={t('contact.meta.description')}>
<PageHero title={t('contact.hero.title')} bgImage={getImageOverride('contact.hero.image') ?? contactHero} />
<section class="section">
<div class="container cgrid">
<div class="cmap" id="map-box">
<button class="btn btn--dark" id="map-load">{t('contact.map.button')}</button>
<p class="cmap__note">{t('contact.map.note')}</p>
<div class="cmap">
<iframe src={`https://www.google.com/maps?q=${mapsQuery}&output=embed`} loading="lazy" title="Mappa InsanityLab" referrerpolicy="no-referrer-when-downgrade"></iframe>
</div>
<ContactForm variant="full" />
</div>
@@ -40,18 +39,10 @@ const mapsQuery = encodeURIComponent(`${t('global.contact.address')}, ${t('globa
<style>
.cgrid { display: grid; grid-template-columns: 1.2fr 1fr; gap: 50px; margin-bottom: 50px; }
.cmap { background: var(--c-bg-alt); min-height: 420px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; }
.cmap iframe { width: 100%; height: 100%; border: 0; min-height: 420px; }
.cmap__note { font-size: .8rem; color: var(--c-text-light); }
.cmap { background: var(--c-bg-alt); min-height: 420px; }
.cmap iframe { display: block; width: 100%; height: 100%; min-height: 420px; border: 0; }
.cinfo { display: grid; grid-template-columns: repeat(3, 1fr); gap: 30px; margin-bottom: 30px; }
.cinfo h3 { font-size: .85rem; text-transform: uppercase; letter-spacing: .16em; }
.cinvite { max-width: 640px; }
@media (max-width: 991px) { .cgrid, .cinfo { grid-template-columns: 1fr; } }
</style>
<script define:vars={{ mapsQuery }}>
document.getElementById('map-load')?.addEventListener('click', () => {
const box = document.getElementById('map-box');
box.innerHTML = `<iframe src="https://www.google.com/maps?q=${mapsQuery}&output=embed" loading="lazy" title="Mappa InsanityLab" referrerpolicy="no-referrer-when-downgrade"></iframe>`;
});
</script>
-4
View File
@@ -6,8 +6,6 @@ import FeatureBlocks from '../components/home/FeatureBlocks.astro';
import QuoteBanner from '../components/home/QuoteBanner.astro';
import TrainingCards from '../components/home/TrainingCards.astro';
import ServicesGrid from '../components/home/ServicesGrid.astro';
import Agenda from '../components/Agenda.astro';
import TeamSection from '../components/home/TeamSection.astro';
import MethodSteps from '../components/home/MethodSteps.astro';
import PartnersStrip from '../components/home/PartnersStrip.astro';
import T from '../components/content/T.astro';
@@ -27,8 +25,6 @@ export const prerender = false;
</section>
<TrainingCards />
<ServicesGrid />
<Agenda />
<TeamSection />
<MethodSteps />
<PartnersStrip />
</Base>
+1 -9
View File
@@ -139,10 +139,6 @@ export const prerender = false;
</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>
@@ -225,10 +221,6 @@ export const prerender = false;
.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; }
@@ -248,7 +240,7 @@ export const prerender = false;
.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 .hero, .lp .launch, .lp .why5, .lp .cards-wrap, .lp .faq-wrap { padding-left: 28px; padding-right: 28px; }
.lp .cards { grid-template-columns: 1fr; }
.lp .hero h1 { font-size: 32px; }
.lp .launch h1 { font-size: 28px; }
+1 -17
View File
@@ -5,9 +5,7 @@ import TrainingCards from '../components/home/TrainingCards.astro';
import Agenda from '../components/Agenda.astro';
import { Image } from 'astro:assets';
import { trainings } from '../data/trainings';
import trialImg from '../assets/img/trial-class.jpg';
import T from '../components/content/T.astro';
import TImg from '../components/content/TImg.astro';
import { t } from '../lib/content';
const images = import.meta.glob<{ default: ImageMetadata }>('../assets/img/training-*.{jpg,png}', { eager: true });
const imgOf = (name: string) => Object.entries(images).find(([p]) => p.includes(name))?.[1]?.default;
@@ -29,7 +27,7 @@ export const prerender = false;
<div class:list={['tblock', { 'tblock--reverse': i % 2 === 1 }]} id={tr.id}>
{imgOf(tr.image) && <Image src={imgOf(tr.image)!} alt={t(`training.${tr.id}.title`)} widths={[500, 900]} sizes="(max-width:767px) 100vw, 45vw" data-reveal={i % 2 === 1 ? 'right' : 'left'} />}
<div data-reveal={i % 2 === 1 ? 'left' : 'right'}>
<h2 data-tag={`training.${tr.id}.title`}>{t(`training.${tr.id}.title`).toLowerCase()}</h2>
<h2 data-tag={`training.${tr.id}.title`}>{t(`training.${tr.id}.title`)}</h2>
<T tag={`training.${tr.id}.description`} as="p" />
<a class="btn" href={tr.cta.href}><T tag={`training.${tr.id}.cta-label`} as="span" /></a>
</div>
@@ -39,25 +37,11 @@ export const prerender = false;
</section>
<Agenda />
<section class="section section--alt">
<div class="container trial">
<TImg tag="training.trial.image" src={trialImg} alt="Lezione di prova InsanityLab" widths={[500, 900]} sizes="(max-width:767px) 100vw, 45vw" data-reveal="left" />
<div data-reveal="right">
<T tag="training.trial.title" as="h2" />
<T tag="training.trial.body" as="p" />
<a class="btn btn--dark" href="/contact">{t('training.trial.cta')}</a>
</div>
</div>
</section>
</Base>
<style>
.tblock { display: grid; grid-template-columns: 1fr 1fr; gap: 60px; align-items: center; padding-block: 40px; scroll-margin-top: calc(var(--header-h) + 20px); }
.tblock--reverse img { order: 2; }
.tblock h2 { letter-spacing: .1em; }
.trial { display: grid; grid-template-columns: 1fr 1.2fr; gap: 60px; align-items: center; }
.trial img { width: 100%; height: auto; max-height: 460px; object-fit: cover; }
@media (max-width: 767px) { .trial { grid-template-columns: 1fr; gap: 24px; } }
@media (max-width: 767px) { .tblock { grid-template-columns: 1fr; gap: 24px; } .tblock--reverse img { order: 0; } }
</style>