Files
InsanityLab-web/.superpowers/sdd/review-ac75720..2fdd1b9.diff
Adriano b913a2bf90 registri dei piani in git (AI-OS#309)
Refs gitea:Adriano/AI-OS#310

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 10:36:26 +02:00

243 lines
10 KiB
Diff

# Review package: ac75720..HEAD
## Commits
2fdd1b9 feat: layout base con header, footer e 404
## Files changed
src/components/ContactForm.astro | 42 ++++++++++++++++++++++++++++++++++
src/components/Footer.astro | 49 ++++++++++++++++++++++++++++++++++++++++
src/components/Header.astro | 45 ++++++++++++++++++++++++++++++++++++
src/layouts/Base.astro | 33 +++++++++++++++++++++++++++
src/pages/404.astro | 14 ++++++++++++
src/pages/index.astro | 6 +++--
6 files changed, 187 insertions(+), 2 deletions(-)
## Diff
diff --git a/src/components/ContactForm.astro b/src/components/ContactForm.astro
new file mode 100644
index 0000000..fec0282
--- /dev/null
+++ b/src/components/ContactForm.astro
@@ -0,0 +1,42 @@
+---
+interface Props { variant?: 'full' | 'footer' }
+const { variant = 'full' } = Astro.props;
+---
+<form class:list={['cform', `cform--${variant}`]} method="post" action="/api/contact" novalidate>
+ <input class="field" name="firstName" placeholder="Nome" required maxlength="100" />
+ {variant === 'full' && <input class="field" name="lastName" placeholder="Cognome" maxlength="100" />}
+ {variant === 'full' && <input class="field" name="phone" placeholder="Telefono" maxlength="40" />}
+ <input class="field" type="email" name="email" placeholder="E-mail" required maxlength="200" />
+ <textarea class="field" name="message" placeholder="Messaggio" required maxlength="5000"></textarea>
+ <input class="hp" type="text" name="website" tabindex="-1" autocomplete="off" />
+ <button class="btn" type="submit">Invia messaggio</button>
+ <p class="form-msg" hidden></p>
+</form>
+
+<script>
+ document.querySelectorAll<HTMLFormElement>('.cform').forEach((form) => {
+ form.addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const msg = form.querySelector<HTMLParagraphElement>('.form-msg')!;
+ const btn = form.querySelector<HTMLButtonElement>('button')!;
+ btn.disabled = true;
+ msg.hidden = true;
+ try {
+ const res = await fetch('/api/contact', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(Object.fromEntries(new FormData(form))),
+ });
+ const data = await res.json();
+ msg.textContent = res.ok ? 'Messaggio inviato, ti ricontatteremo al più presto.' : (data.error ?? 'Errore di invio, riprova più tardi.');
+ msg.className = `form-msg ${res.ok ? 'form-msg--ok' : 'form-msg--err'}`;
+ if (res.ok) form.reset();
+ } catch {
+ msg.textContent = 'Errore di rete, riprova più tardi.';
+ msg.className = 'form-msg form-msg--err';
+ }
+ msg.hidden = false;
+ btn.disabled = false;
+ });
+ });
+</script>
diff --git a/src/components/Footer.astro b/src/components/Footer.astro
new file mode 100644
index 0000000..1546466
--- /dev/null
+++ b/src/components/Footer.astro
@@ -0,0 +1,49 @@
+---
+import { site } from '../data/site';
+import ContactForm from './ContactForm.astro';
+const year = new Date().getFullYear();
+---
+<footer class="ftr">
+ <div class="container ftr__grid">
+ <div>
+ <img src="/img/logo-light.png" alt="InsanityLab" width="140" loading="lazy" />
+ <p class="ftr__tag">PERFORMANCE.&nbsp; BALANCE.&nbsp; LONGEVITY.</p>
+ <p>Promuoviamo il movimento come cura di sé. Abitudini sostenibili per benessere fisico e mentale.</p>
+ <p>{site.phone}<br />{site.address}, {site.cityLine}</p>
+ {site.hours.map((h) => <p class="ftr__hours">{h}</p>)}
+ </div>
+ <nav aria-label="training">
+ <h4>Training</h4>
+ {site.footerTraining.map((l) => <a href={l.href}>{l.label}</a>)}
+ </nav>
+ <nav aria-label="programmi">
+ <h4>Programmi</h4>
+ {site.footerPrograms.map((l) => <a href={l.href}>{l.label}</a>)}
+ </nav>
+ <div>
+ <h4>Contact</h4>
+ <ContactForm variant="footer" />
+ </div>
+ </div>
+ <div class="ftr__bar">
+ <div class="container ftr__bar-in">
+ <p>© {year} <strong>Insanitylab</strong>, All Rights Reserved</p>
+ <p>Seguici {site.socials.map((s) => <a href={s.url} rel="noopener" target="_blank">{s.label}</a>)}</p>
+ </div>
+ </div>
+</footer>
+
+<style>
+ .ftr { background: var(--c-dark); color: #b9aea4; font-size: .88rem; }
+ .ftr a { color: #b9aea4; text-decoration: none; display: block; padding: 4px 0; }
+ .ftr a:hover { color: #fff; }
+ .ftr h4 { color: #fff; text-transform: uppercase; letter-spacing: .18em; font-size: .78rem; margin-bottom: 18px; }
+ .ftr__grid { display: grid; grid-template-columns: 1.3fr 1fr 1fr 1.3fr; gap: 40px; padding-block: 70px; }
+ .ftr__tag { font-family: var(--font-heading); font-size: .7rem; letter-spacing: .2em; color: #fff; }
+ .ftr__hours { margin: 0; }
+ .ftr__bar { border-top: 1px solid rgba(255,255,255,.08); padding-block: 18px; }
+ .ftr__bar-in { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px; }
+ .ftr__bar-in a { display: inline; margin-left: 12px; }
+ @media (max-width: 991px) { .ftr__grid { grid-template-columns: 1fr 1fr; } }
+ @media (max-width: 600px) { .ftr__grid { grid-template-columns: 1fr; } }
+</style>
diff --git a/src/components/Header.astro b/src/components/Header.astro
new file mode 100644
index 0000000..7c62429
--- /dev/null
+++ b/src/components/Header.astro
@@ -0,0 +1,45 @@
+---
+import { site } from '../data/site';
+const path = Astro.url.pathname;
+---
+<header class="hdr" id="site-header">
+ <div class="container hdr__in">
+ <a href="/" class="hdr__logo"><img src="/img/logo-dark.png" alt="InsanityLab" width="150" /></a>
+ <nav class="hdr__nav" id="site-nav" aria-label="principale">
+ {site.navigation.map((item) => (
+ <a href={item.href} class:list={['hdr__link', { 'is-active': path === item.href || (item.href !== '/' && path.startsWith(item.href)) }]}>{item.label}</a>
+ ))}
+ </nav>
+ <button class="hdr__burger" id="nav-toggle" aria-label="Apri menu" aria-expanded="false">
+ <span></span><span></span><span></span>
+ </button>
+ </div>
+</header>
+
+<style>
+ .hdr { position: sticky; top: 0; z-index: 100; background: #fff; transition: box-shadow .2s; }
+ .hdr.is-scrolled { box-shadow: 0 2px 14px rgba(0,0,0,.08); }
+ .hdr__in { display: flex; align-items: center; justify-content: space-between; height: var(--header-h); }
+ .hdr__nav { display: flex; gap: 34px; }
+ .hdr__link { font-family: var(--font-heading); font-size: .72rem; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; text-decoration: none; color: var(--c-heading); }
+ .hdr__link:hover, .hdr__link.is-active { color: var(--c-accent-dark); }
+ .hdr__burger { display: none; background: none; border: 0; cursor: pointer; padding: 8px; }
+ .hdr__burger span { display: block; width: 22px; height: 2px; background: var(--c-heading); margin: 5px 0; }
+ @media (max-width: 991px) {
+ .hdr__nav { display: none; position: absolute; top: var(--header-h); left: 0; right: 0; background: #fff; flex-direction: column; gap: 0; padding: 10px 20px 20px; box-shadow: 0 10px 20px rgba(0,0,0,.08); }
+ .hdr__nav.is-open { display: flex; }
+ .hdr__link { padding: 12px 0; }
+ .hdr__burger { display: block; }
+ }
+</style>
+
+<script>
+ const header = document.getElementById('site-header')!;
+ const toggle = document.getElementById('nav-toggle')!;
+ const nav = document.getElementById('site-nav')!;
+ addEventListener('scroll', () => header.classList.toggle('is-scrolled', scrollY > 10), { passive: true });
+ toggle.addEventListener('click', () => {
+ const open = nav.classList.toggle('is-open');
+ toggle.setAttribute('aria-expanded', String(open));
+ });
+</script>
diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro
new file mode 100644
index 0000000..4b19758
--- /dev/null
+++ b/src/layouts/Base.astro
@@ -0,0 +1,33 @@
+---
+import '@fontsource/montserrat/400.css';
+import '@fontsource/montserrat/500.css';
+import '@fontsource/montserrat/600.css';
+import '@fontsource/montserrat/700.css';
+import '@fontsource/open-sans/400.css';
+import '@fontsource/open-sans/600.css';
+import '../styles/global.css';
+import Header from '../components/Header.astro';
+import Footer from '../components/Footer.astro';
+
+interface Props { title: string; description?: string }
+const { title, description = 'InsanityLab — Performance. Balance. Longevity. Allenamento personalizzato, piccoli gruppi e benessere a Bologna.' } = Astro.props;
+---
+<!doctype html>
+<html lang="it">
+ <head>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <title>{title} — InsanityLab</title>
+ <meta name="description" content={description} />
+ <meta property="og:title" content={`${title} — InsanityLab`} />
+ <meta property="og:description" content={description} />
+ <meta property="og:type" content="website" />
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
+ <link rel="sitemap" href="/sitemap-index.xml" />
+ </head>
+ <body>
+ <Header />
+ <main><slot /></main>
+ <Footer />
+ </body>
+</html>
diff --git a/src/pages/404.astro b/src/pages/404.astro
new file mode 100644
index 0000000..9aa6d78
--- /dev/null
+++ b/src/pages/404.astro
@@ -0,0 +1,14 @@
+---
+import Base from '../layouts/Base.astro';
+export const prerender = true;
+---
+<Base title="Pagina non trovata">
+ <section class="section" style="text-align:center; padding-block:140px;">
+ <div class="container">
+ <p class="eyebrow">Errore 404</p>
+ <h1>Pagina non trovata</h1>
+ <p>La pagina che cerchi non esiste o è stata spostata.</p>
+ <a class="btn" href="/">Torna alla home</a>
+ </div>
+ </section>
+</Base>
diff --git a/src/pages/index.astro b/src/pages/index.astro
index ddaf917..86dcea2 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,5 +1,7 @@
---
+import Base from '../layouts/Base.astro';
export const prerender = true;
---
-<html lang="it"><head><title>InsanityLab</title></head>
-<body><h1>InsanityLab — in costruzione</h1></body></html>
+<Base title="Performance. Balance. Longevity.">
+ <section class="section"><div class="container"><h1>Homepage in costruzione</h1></div></section>
+</Base>