diff --git a/apps/web/messages/it.json b/apps/web/messages/it.json index a3188a5..30c4c3a 100644 --- a/apps/web/messages/it.json +++ b/apps/web/messages/it.json @@ -1,9 +1,12 @@ { "Home": { "title": "Una keto italiana, sostenibile", + "titleAccent": "sostenibile", "tagline": "Il sentiero che ti accompagna dalla prima settimana di adattamento al mantenimento — settimana dopo settimana, piatto dopo piatto.", "ctaPrimary": "Iscriviti", "ctaSecondary": "Accedi", + "ctaTryFree": "Inizia gratis", + "ctaTryFreeHint": "Niente carta, niente spam. Disinstalla quando vuoi.", "disclaimer": "KetoPath è uno strumento di stile di vita: suggerisce, non prescrive. Le indicazioni nutrizionali non sostituiscono il parere del tuo medico.", "signedInAs": "Accesso effettuato come {name}", "signOut": "Esci", @@ -17,7 +20,42 @@ "greeting": "Buongiorno, {name}.", "principle1": "Personalizzazione totale: ciò che vedi è solo ciò che ti riguarda.", "principle2": "Cucina italiana e mediterranea: ingredienti veri, di stagione, reperibili.", - "principle3": "Progressività: dalla fase intensiva al mantenimento, senza strappi." + "principle3": "Progressività: dalla fase intensiva al mantenimento, senza strappi.", + "stat1Label": "Fasi del percorso", + "stat1Value": "3", + "stat1Hint": "Intensiva · Transizione · Mantenimento", + "stat2Label": "Ricette mediterranee", + "stat2Value": "96+", + "stat2Hint": "Tutte stagionali, italiane", + "stat3Label": "Costo del piano", + "stat3Value": "0€", + "stat3Hint": "Gratis durante la beta", + "featureSectionEyebrow": "Cosa fa per te", + "featureSectionTitle": "Tre strumenti, un solo ritmo.", + "feature1Eyebrow": "Cucina", + "feature1Title": "Piano settimanale che si adatta a te", + "feature1Body": "Ogni domenica un menù di sette giorni costruito sui tuoi macros, le tue esclusioni e il tempo che hai in cucina. La lista della spesa si genera da sola.", + "feature2Eyebrow": "Digiuno", + "feature2Title": "Digiuno intermittente, senza ansia", + "feature2Body": "Sei protocolli, dal 14:10 al 5:2. Il timer ti accompagna con micro-promemoria di idratazione, elettroliti, e una proposta morbida per rompere il digiuno.", + "feature3Eyebrow": "Tracking", + "feature3Title": "Numeri che parlano, non urlano", + "feature3Body": "Una pesata a settimana, qualche misura quando vuoi, una foto se ti va. Trend lineare, soglia d'allarme, esportazione PDF per il medico.", + "previewSectionEyebrow": "Sotto il cofano", + "previewSectionTitle": "L'app, in tre schermate.", + "previewSectionBody": "Niente dashboard infinite. Tre superfici essenziali: il piano di oggi, la sessione di digiuno, la pesata della settimana.", + "previewPlanLabel": "Mercoledì · giorno III", + "previewPlanRecipe1": "Frittata di spinaci", + "previewPlanRecipe2": "Tagliata con rucola", + "previewPlanRecipe3": "Branzino al cartoccio", + "previewFastLabel": "16:8 · in corso", + "previewFastTime": "08:42", + "previewFastHint": "Ancora 7h 18m alla finestra alimentare.", + "previewWeightLabel": "−4,2 kg in 8 settimane", + "ctaSectionTitle": "Pronti a partire?", + "ctaSectionBody": "Sette passaggi, dieci minuti, e il tuo primo piano è pronto. Senza carta di credito.", + "principlesEyebrow": "Tre principi non negoziabili", + "footerCopyright": "KetoPath — anno I, Italia." }, "Welcome": { "title": "Prima di iniziare", diff --git a/apps/web/src/app/[locale]/fasting/page.tsx b/apps/web/src/app/[locale]/fasting/page.tsx index 5e8a8f5..54acabe 100644 --- a/apps/web/src/app/[locale]/fasting/page.tsx +++ b/apps/web/src/app/[locale]/fasting/page.tsx @@ -3,6 +3,7 @@ import { redirect } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { setRequestLocale } from 'next-intl/server'; +import { CursorGlow } from '@/components/cursor-glow'; import { Masthead } from '@/components/masthead'; import { getServerSession } from '@/lib/auth'; @@ -41,29 +42,58 @@ function FastingPageContent({ const t = useTranslations('Fasting'); return ( -
- -
-

Capitolo VII

-

- {t('title')} - . -

-

- {t('subtitle')} -

+
+
+ +
+ +
+
+ + VII + +
+

+ Capitolo VII — Digiuno +

+
+

+ + {t('title')} + . + +

+

+ {t('subtitle')} +

-
+
- +
+ - + -
-

{t('historyTitle')}

- -
-
+
+

{t('historyTitle')}

+ +
+
+
+
); } diff --git a/apps/web/src/app/[locale]/onboarding/page.tsx b/apps/web/src/app/[locale]/onboarding/page.tsx index 37433d2..c77c974 100644 --- a/apps/web/src/app/[locale]/onboarding/page.tsx +++ b/apps/web/src/app/[locale]/onboarding/page.tsx @@ -3,6 +3,7 @@ import { redirect } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { setRequestLocale } from 'next-intl/server'; +import { CursorGlow } from '@/components/cursor-glow'; import { Masthead } from '@/components/masthead'; import { getServerSession } from '@/lib/auth'; @@ -47,24 +48,57 @@ function OnboardingPageContent({ const t = useTranslations('Onboarding'); return ( -
- -
-

Capitolo III

-

- {t('title')} - . -

-

- {t('subtitle')} -

+
+
+ +
+ +
+
-
+ {/* Chapter "III" gigante in stroke */} + + III + - -
+ {/* Eyebrow verticale destra */} +
+

+ Capitolo III — Inizio +

+
-
+ {/* Titolo bleed-left */} +

+ {t('title')} +

+ +

+ {t('subtitle')} +

+ +
+ +
+ +
+
+ +
+
); } diff --git a/apps/web/src/app/[locale]/page.tsx b/apps/web/src/app/[locale]/page.tsx index 780e4ce..ec2e5b7 100644 --- a/apps/web/src/app/[locale]/page.tsx +++ b/apps/web/src/app/[locale]/page.tsx @@ -2,8 +2,8 @@ import Link from 'next/link'; import { useTranslations } from 'next-intl'; import { setRequestLocale } from 'next-intl/server'; +import { CursorGlow } from '@/components/cursor-glow'; import { Masthead } from '@/components/masthead'; -import { Button } from '@/components/ui/button'; import { getServerSession } from '@/lib/auth'; import { SignOutButton } from './sign-out-button'; @@ -16,114 +16,936 @@ export default async function HomePage({ params: { locale } }: { params: { local } function HomeContent({ userName }: { userName: string | null }) { + return ( +
+
+ + {userName ? : } +
+
+ ); +} + +/* ───────────────────────────────────────────────────────────────────────── + * Marketing landing — Asymmetric, drastico. + * Mosse: chapter markers giganti come art di sfondo, titoli che sbordano + * il viewport (bleed-left), mockup ruotato che si sovrappone al titolo, + * eyebrow verticali, card ruotate ad angoli diversi. + * ───────────────────────────────────────────────────────────────────────── */ +function MarketingLanding() { + return ( +
+ + + + + + + + + + ); +} + +/* HERO — title rompe il bordo sinistro del viewport, mockup absolute + * sovrappone parte del titolo come un timbro. Eyebrow verticale a destra. */ +function Hero() { const t = useTranslations('Home'); + const title = t('title'); + const accent = t('titleAccent'); + const accentIdx = title.toLowerCase().indexOf(accent.toLowerCase()); + const before = accentIdx >= 0 ? title.slice(0, accentIdx).trim() : title; + const accentText = accentIdx >= 0 ? title.slice(accentIdx, accentIdx + accent.length) : ''; return ( -
- -
-
-

N. 01 — Manifesto

-

- {t('title')} - . -

-

- {t('tagline')} -

-
+
+ +
+
- -
- -
- -
-
-

- {t('disclaimer')} -

-
-
-

Tre principi

-
    -
  1. - I - {t('principle1')} -
  2. -
  3. - II - {t('principle2')} -
  4. -
  5. - III - {t('principle3')} -
  6. -
-
-
- -
-
- ); -} - -function SignedOutBlock({ t }: { t: ReturnType> }) { - return ( -
- - -
- ); -} - -function SignedInBlock({ - userName, - t, -}: { - userName: string; - t: ReturnType>; -}) { - return ( -
-
-

- {t.rich('greeting', { - name: () => {userName}, - })} + {/* Eyebrow VERTICALE sul bordo destro */} +

+

+ N. 01 — Manifesto · Anno I — 2026

-
    - - - - - -
- + + {/* Titolo che sborda il bordo sinistro del viewport */} +

+ {before} + italiana, + + {accentText} + . + +

+ + {/* Mockup-ring assoluto: si sovrappone al titolo da destra, + ruotato leggermente come un timbro. */} +
+ +
+ + {/* Tagline + CTA: posizioni completamente sganciate, indentate diverse */} +
+

+ {t('tagline')} +

+
+ + {t('ctaTryFree')} + + → + + + + {t('ctaSecondary')} + +

+ {t('ctaTryFreeHint')} +

+
+
+ + ); +} + +function PhaseRingsArt() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + {[ + { angle: -90, label: 'I' }, + { angle: 30, label: 'II' }, + { angle: 150, label: 'III' }, + ].map(({ angle, label }) => { + const rad = (angle * Math.PI) / 180; + const cx = 300 + Math.cos(rad) * 280; + const cy = 300 + Math.sin(rad) * 280; + return ( + + + + {label} + + + ); + })} + + ); +} + +function Marquee() { + const tokens = [ + 'KETO ITALIANA', + 'MEDITERRANEA', + 'PROGRESSIVA', + 'PERSONALIZZATA', + 'NIENTE FAME', + 'ZERO CARTE', + '96+ RICETTE', + '3 FASI', + 'DIGIUNO INTERMITTENTE', + 'GDPR-FIRST', + ]; + const doubled = [...tokens, ...tokens]; + return ( +
+
+ {doubled.map((tok, i) => ( + + + {tok} + + + ✦ + + + ))} +
+
+ ); +} + +/* STATS — Una stat occupa 60% della larghezza con numero a 28vw, le altre + * due flottano in absolute a posizioni diverse. Sezione anche bleed a destra. */ +function StatsScattered() { + const t = useTranslations('Home'); + return ( +
+ {/* Chapter marker di sfondo: "II" gigante, stroke trasparente */} + + II + + + {/* Stat 1 — gigante, sborda a sinistra */} +
+

{t('stat1Label')}

+

+ {t('stat1Value')} +

+

+ {t('stat1Hint')} +

+
+ + {/* Stat 2 — assoluta in alto a destra */} +
+

{t('stat2Label')}

+

+ {t('stat2Value')} +

+

+ {t('stat2Hint')} +

+
+ + {/* Stat 3 — assoluta in basso a destra, sotto la stat 2 */} +
+

{t('stat3Label')}

+

+ {t('stat3Value')} +

+

+ {t('stat3Hint')} +

+
+
+ ); +} + +/* FEATURES — ognuna è una composizione diversa con un chapter marker + * gigante in stroke come art di sfondo. Card ruotate a -2°, 0°, +2°. */ +function FeaturesAsymmetric() { + const t = useTranslations('Home'); + return ( +
+
+

+ {t('featureSectionEyebrow')} +

+

+ {t('featureSectionTitle')} +

+
+ + {/* Feature 1 — chapter "01" gigante a sinistra, card a destra */} + } + /> + + {/* Feature 2 — chapter "02" a destra, card a sinistra (flippata) */} + } + /> + + {/* Feature 3 — chapter "03" centrale, card al centro (rompe il pattern) */} + } + /> +
+ ); +} + +function FeatureBlock({ + chapter, + eyebrow, + title, + body, + accent, + align, + rotate, + icon, +}: { + chapter: string; + eyebrow: string; + title: string; + body: string; + accent: 'oliva' | 'pomodoro' | 'oro'; + align: 'left' | 'right' | 'center'; + rotate: string; + icon: React.ReactNode; +}) { + const accentClass = + accent === 'oliva' ? 'text-oliva' : accent === 'pomodoro' ? 'text-pomodoro' : 'text-oro'; + const tintGrad = + accent === 'oliva' + ? 'linear-gradient(135deg, hsl(var(--oliva) / 0.18) 0%, hsl(var(--carta-light)) 60%, transparent 100%)' + : accent === 'pomodoro' + ? 'linear-gradient(135deg, hsl(var(--pomodoro) / 0.18) 0%, hsl(var(--carta-light)) 60%, transparent 100%)' + : 'linear-gradient(135deg, hsl(var(--oro) / 0.18) 0%, hsl(var(--carta-light)) 60%, transparent 100%)'; + + // Posizionamento del chapter marker per ogni align + const chapterPosition = + align === 'right' + ? '-left-4 top-[-2rem]' + : align === 'left' + ? '-right-4 top-[-3rem]' + : 'left-1/2 top-[-4rem] -translate-x-1/2'; + + // Posizionamento della card per ogni align + const cardCols = + align === 'right' + ? 'md:col-start-6 md:col-span-7' + : align === 'left' + ? 'md:col-start-1 md:col-span-7' + : 'md:col-start-3 md:col-span-8'; + + return ( +
+ {/* Chapter marker gigante in stroke, fa da art di sfondo */} + + {chapter} + + + {/* Card della feature, ruotata */} +
+
+
+
+

{eyebrow}

+

+ {title} +

+

+ {body} +

+
+
+ {icon} +
+
+
); } -function NavItem({ href, label, eyebrow }: { href: string; label: string; eyebrow: string }) { +function PlateIcon({ size = 60 }: { size?: number }) { + return ( + + + + + + + + + ); +} + +function TimerIcon({ size = 60 }: { size?: number }) { + return ( + + + + + + + + + ); +} + +function TrendIcon({ size = 60 }: { size?: number }) { + return ( + + + + + + + + + + ); +} + +/* APP SHOWCASE — mockup ruotato che sborda a destra del viewport. */ +function AppShowcaseOffset() { + const t = useTranslations('Home'); + return ( +
+
+
+ + {/* Chapter marker "IV" gigante */} + + IV + + +
+
+

{t('previewSectionEyebrow')}

+

+ {t('previewSectionTitle')} +

+

+ {t('previewSectionBody')} +

+
+ + {/* Mockup sborda DESTRA del viewport, ruotato +3° */} +
+
+ +
+
+
+
+ ); +} + +function BrowserMockup({ t }: { t: ReturnType> }) { + return ( +
+
+ + + + ketopath.app/plan +
+
+ + + +
+
+ ); +} + +function ShowcasePlanCard({ t }: { t: ReturnType> }) { + return ( +
+
+

III

+

+ {t('previewPlanLabel')} +

+
+
    + + + +
+
+ kcal + 1.510 +
+
+ ); +} + +function ShowcaseRow({ numeral, label, kcal }: { numeral: string; label: string; kcal: string }) { + return ( +
  • + + {numeral} + {label} + + {kcal} +
  • + ); +} + +function ShowcaseFastCard({ t }: { t: ReturnType> }) { + return ( +
    +
    +

    II

    +

    + {t('previewFastLabel')} +

    +
    + + + + + + + + + + + {t('previewFastTime')} + + +

    + {t('previewFastHint')} +

    +
    + ); +} + +function ShowcaseWeightCard({ t }: { t: ReturnType> }) { + return ( +
    +
    +

    III

    +

    −4,2 kg

    +
    + + + + + + + + + + {[0, 25, 50, 75, 100, 125, 150, 175, 200].map((x, i) => { + const ys = [30, 38, 36, 50, 56, 70, 76, 84, 92]; + return ; + })} + +

    + {t('previewWeightLabel')} +

    +
    + ); +} + +/* CTA finale — full-bleed dark, "?" gigante che sborda in alto a destra, + * titolo ancorato in basso a sinistra. */ +function FinalCta() { + const t = useTranslations('Home'); + const titleNoMark = t('ctaSectionTitle').replace(/\?$/, ''); + return ( +
    +
    +
    + + {/* "?" GIGANTE che sborda fuori dal lato destro e in alto */} + + ? + + +
    + {/* Pull-quote in alto, indentata */} +

    + {t('ctaSectionBody')} +

    + + {/* Titolo gigante in basso, ancorato a sinistra */} +

    + {titleNoMark} + . +

    + + {/* CTA in basso a destra, defilato */} +
    + + {t('ctaTryFree')} + + → + + +

    + {t('ctaTryFreeHint')} +

    +
    +
    +
    + ); +} + +function Principles() { + const t = useTranslations('Home'); + return ( +
    + + V + + +

    {t('principlesEyebrow')}

    +
      +
    1. + I + {t('principle1')} +
    2. +
    3. + II + {t('principle2')} +
    4. +
    5. + III + {t('principle3')} +
    6. +
    +
    + ); +} + +function Disclaimer() { + const t = useTranslations('Home'); + return ( +
    +

    + {t('disclaimer')} +

    +

    + {t('footerCopyright')} +

    +
    + ); +} + +function SignedInDashboard({ userName }: { userName: string }) { + const t = useTranslations('Home'); + return ( +
    + +
    +
    +
    + + {/* Chapter marker gigante "I" come art di sfondo */} + + I + + + {/* Eyebrow verticale a destra */} +
    +

    {t('lettura')}

    +
    + + {/* Greeting bleed-left, sborda fuori dal container */} +

    + Buongiorno, + + {userName} + . + +

    + +

    + {t('tagline')} +

    + + {/* Nav asimmetrica: card grande "/plan" a sinistra, le altre 4 in colonna a destra */} +
    + +
      + + + + +
    • + +
    • +
    +
    + + +
    + ); +} + +function DashboardHero({ + href, + eyebrow, + title, + accent, +}: { + href: string; + eyebrow: string; + title: string; + accent: 'oliva' | 'pomodoro' | 'oro'; +}) { + const accentClass = + accent === 'oliva' ? 'text-oliva' : accent === 'pomodoro' ? 'text-pomodoro' : 'text-oro'; + const tintGrad = + accent === 'oliva' + ? 'linear-gradient(135deg, hsl(var(--oliva) / 0.18) 0%, hsl(var(--carta-light)) 70%)' + : accent === 'pomodoro' + ? 'linear-gradient(135deg, hsl(var(--pomodoro) / 0.18) 0%, hsl(var(--carta-light)) 70%)' + : 'linear-gradient(135deg, hsl(var(--oro) / 0.18) 0%, hsl(var(--carta-light)) 70%)'; + return ( + +
    +
    +

    {eyebrow}

    +
    +

    + {title} +

    +

    + → +

    +
    +
    + + ); +} + +function NavItem({ + href, + label, + eyebrow, + chapter, +}: { + href: string; + label: string; + eyebrow: string; + chapter?: string; +}) { return (
  • - - {eyebrow} - {label} + + {chapter ? ( + + {chapter} + + ) : null} + + {eyebrow} + {label} + - -
    -
    -
    -

    Capitolo V

    -

    +
    +
    + +
    + +
    +
    + + {/* Chapter "V" gigante in stroke */} + + V + + + {/* Eyebrow verticale destra */} +
    +

    + Capitolo V — Cucina +

    +
    + + {/* Titolo che sborda */} +

    + {t('title')} . -

    -

    - {plan ? formatWeek(plan.weekStart) : t('noPlan')} -

    - {plan?.fastingProtocol ? ( -

    - {t('window')}{' '} - - {t(`protocolLabel.${plan.fastingProtocol}`)} - -

    - ) : null} - {plan?.currentPhase === 'TRANSITION' && plan.phase2Week ? ( -

    - {t('phase2Eyebrow')}{' '} - - {t('phase2Week', { n: plan.phase2Week })} - -

    - ) : plan?.currentPhase === 'MAINTENANCE' ? ( -

    - {t('phase3Eyebrow')} -

    - ) : null} -
    -
    -
    - -
    - {plan ? ( - <> - - {t('linkShopping')} → - - - {t('exportPdf')} ↗ - - - ) : null} -
    -
    + +

    -
    - - {plan ? ( -
    - +
    +
    +

    + {plan ? formatWeek(plan.weekStart) : t('noPlan')} +

    +
    + {plan?.fastingProtocol ? ( + + {t('window')} + + {t(`protocolLabel.${plan.fastingProtocol}`)} + + + ) : null} + {plan?.currentPhase === 'TRANSITION' && plan.phase2Week ? ( + + {t('phase2Eyebrow')} + + {t('phase2Week', { n: plan.phase2Week })} + + + ) : plan?.currentPhase === 'MAINTENANCE' ? ( + {t('phase3Eyebrow')} + ) : null} +
    +
    +
    +
    + +
    + {plan ? ( + <> + + {t('linkShopping')} → + + + {t('exportPdf')} ↗ + + + ) : null} +
    - ) : null} -
    + +
    + + {plan ? ( +
    + +
    + ) : null} + +
  • ); } diff --git a/apps/web/src/app/[locale]/profile/page.tsx b/apps/web/src/app/[locale]/profile/page.tsx index 43830a1..a125b2b 100644 --- a/apps/web/src/app/[locale]/profile/page.tsx +++ b/apps/web/src/app/[locale]/profile/page.tsx @@ -3,6 +3,7 @@ import { redirect } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { setRequestLocale } from 'next-intl/server'; +import { CursorGlow } from '@/components/cursor-glow'; import { Masthead } from '@/components/masthead'; import { getServerSession } from '@/lib/auth'; @@ -56,48 +57,75 @@ function ProfilePageContent({ const t = useTranslations('Profile'); return ( -
    - -
    -

    Capitolo IV

    -

    - {t('title')} - . -

    -

    - {t('subtitle')} -

    +
    +
    + +
    + +
    +
    + + IV + +
    +

    + Capitolo IV — Profilo +

    +
    +

    + + {t('title')} + . + +

    +

    + {t('subtitle')} +

    -
    +
    -
    - -
    +
    + +
    -
    +
    -
    - -
    +
    + +
    -
    +
    -
    - -
    +
    + +
    -
    +
    -
    - -
    +
    + +
    -
    +
    -
    - -
    -
    +
    + +
    +
    +
    ); } diff --git a/apps/web/src/app/[locale]/shopping/page.tsx b/apps/web/src/app/[locale]/shopping/page.tsx index ba3bf43..96f60f7 100644 --- a/apps/web/src/app/[locale]/shopping/page.tsx +++ b/apps/web/src/app/[locale]/shopping/page.tsx @@ -4,6 +4,7 @@ import { redirect } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { setRequestLocale } from 'next-intl/server'; +import { CursorGlow } from '@/components/cursor-glow'; import { Masthead } from '@/components/masthead'; import { Button } from '@/components/ui/button'; import { getServerSession } from '@/lib/auth'; @@ -51,52 +52,78 @@ function ShoppingPageContent({ list }: { list: ShoppingList | null }) { ); return ( -
    - -
    -
    -
    -

    Capitolo IX

    -

    - {t('title')} - . -

    -

    - {list ? formatWeek(list.plan.weekStart) : t('noPlan')} +

    +
    + +
    + +
    +
    + + IX + +
    +

    + Capitolo IX — La spesa

    - {list ? ( - + ) : ( +
    + +
    + )}
    - ) : null} -
    -
    +
    + + {list ? ( +
    + +
    + ) : null} +
    + +
    +
    ); } diff --git a/apps/web/src/app/[locale]/tracking/page.tsx b/apps/web/src/app/[locale]/tracking/page.tsx index 199a7fd..7d8794a 100644 --- a/apps/web/src/app/[locale]/tracking/page.tsx +++ b/apps/web/src/app/[locale]/tracking/page.tsx @@ -3,6 +3,7 @@ import { redirect } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { setRequestLocale } from 'next-intl/server'; +import { CursorGlow } from '@/components/cursor-glow'; import { Masthead } from '@/components/masthead'; import { getServerSession } from '@/lib/auth'; @@ -63,59 +64,86 @@ function TrackingPageContent({ const alertActive = alertKg != null && latest != null && latest.weightKg >= alertKg; return ( -
    - -
    -

    Capitolo VI

    -

    - {t('title')} - . -

    -

    - {t('subtitle')} -

    - -

    - +

    + +
    + +
    +
    + - {t('exportPdf')} ↗ - -

    - -
    - - - - {alertActive && alertKg != null && latest != null ? ( - - ) : null} +
    +

    + + {t('title')} + . + +

    +

    + {t('subtitle')} +

    -
    -
    -

    {t('newEntry')}

    - -
    - -
    -
    +

    + + {t('exportPdf')} ↗ + +

    + +
    + + + + {alertActive && alertKg != null && latest != null ? ( + + ) : null} + +
    +
    +

    {t('newEntry')}

    + +
    + +
    +
    +
    ); } diff --git a/apps/web/src/components/cursor-glow.tsx b/apps/web/src/components/cursor-glow.tsx new file mode 100644 index 0000000..25c5b29 --- /dev/null +++ b/apps/web/src/components/cursor-glow.tsx @@ -0,0 +1,76 @@ +'use client'; + +import { useEffect, useRef } from 'react'; + +/** + * Cursor-tracking glow — un alone radiale che segue il puntatore. + * Si appiccica al parent posizionato relative/absolute. Rispetta + * `prefers-reduced-motion` (in quel caso resta centrato). + */ +export function CursorGlow({ + className = '', + color = 'hsl(var(--pomodoro))', + size = 520, +}: { + className?: string; + color?: string; + size?: number; +}) { + const ref = useRef(null); + + useEffect(() => { + const el = ref.current; + if (!el) return; + const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (reduce) return; + + const parent = el.parentElement; + if (!parent) return; + + let raf = 0; + let targetX = parent.clientWidth / 2; + let targetY = parent.clientHeight / 3; + let currentX = targetX; + let currentY = targetY; + + function onMove(e: PointerEvent): void { + const rect = parent!.getBoundingClientRect(); + targetX = e.clientX - rect.left; + targetY = e.clientY - rect.top; + } + + function loop(): void { + // Easing: avvicino la posizione corrente al target con un delta morbido. + currentX += (targetX - currentX) * 0.12; + currentY += (targetY - currentY) * 0.12; + if (el) { + el.style.transform = `translate3d(${currentX - size / 2}px, ${currentY - size / 2}px, 0)`; + } + raf = requestAnimationFrame(loop); + } + + parent.addEventListener('pointermove', onMove); + raf = requestAnimationFrame(loop); + return () => { + cancelAnimationFrame(raf); + parent.removeEventListener('pointermove', onMove); + }; + }, [size]); + + return ( +
    + ); +} diff --git a/apps/web/src/components/editorial-atmosphere.tsx b/apps/web/src/components/editorial-atmosphere.tsx new file mode 100644 index 0000000..c0b63c9 --- /dev/null +++ b/apps/web/src/components/editorial-atmosphere.tsx @@ -0,0 +1,91 @@ +import { CursorGlow } from './cursor-glow'; + +/** + * Atmosfera di pagina riusabile: 2 mesh-blob fluttuanti + cursor glow + * opzionale. Va inserita come primo figlio di un `relative overflow-hidden`, + * sotto al contenuto principale. + */ +export function EditorialAtmosphere({ + variant = 'default', + withCursor = false, +}: { + variant?: 'default' | 'pomodoro' | 'oliva' | 'oro'; + withCursor?: boolean; +}) { + // Combinazione di blob per ogni variante. + const blobs = + variant === 'pomodoro' + ? [ + { + kind: 'pomodoro', + pos: '-left-32 top-12', + size: 'h-[28rem] w-[28rem]', + anim: 'animate-float-x', + }, + { kind: 'oro', pos: '-right-24 top-64', size: 'h-80 w-80', anim: 'animate-float-y' }, + ] + : variant === 'oliva' + ? [ + { + kind: 'oliva', + pos: '-right-32 top-12', + size: 'h-[28rem] w-[28rem]', + anim: 'animate-float-y', + }, + { + kind: 'pomodoro', + pos: '-left-24 top-64', + size: 'h-72 w-72', + anim: 'animate-float-x', + }, + ] + : variant === 'oro' + ? [ + { + kind: 'oro', + pos: 'right-1/4 top-12', + size: 'h-[24rem] w-[24rem]', + anim: 'animate-float-z', + }, + { + kind: 'oliva', + pos: '-left-24 bottom-12', + size: 'h-72 w-72', + anim: 'animate-float-y', + }, + ] + : [ + { + kind: 'oliva', + pos: '-right-32 top-12', + size: 'h-80 w-80', + anim: 'animate-float-y', + }, + { + kind: 'pomodoro', + pos: '-left-24 top-64', + size: 'h-72 w-72', + anim: 'animate-float-x', + }, + ]; + + const cursorColor = + variant === 'pomodoro' + ? 'hsl(var(--pomodoro))' + : variant === 'oro' + ? 'hsl(var(--oro))' + : 'hsl(var(--oliva))'; + + return ( + <> + {withCursor ? : null} + {blobs.map((b, i) => ( +
    + ))} + + ); +} diff --git a/apps/web/src/components/editorial-header.tsx b/apps/web/src/components/editorial-header.tsx new file mode 100644 index 0000000..f8b77c2 --- /dev/null +++ b/apps/web/src/components/editorial-header.tsx @@ -0,0 +1,76 @@ +/** + * Header tipografico riusabile in stile "asimmetrico off-grid". + * Chapter marker gigante (numero romano) come art di sfondo, + * eyebrow defilata, titolo che può sbordare dal bordo sinistro, + * sottotitolo indentato in colonna stretta. + * + * Va wrappato in un padre `relative overflow-hidden` (la pagina lo è + * già di norma). Il chapter marker è in stroke trasparente, decorativo. + */ +export function EditorialHeader({ + chapter, + eyebrow, + title, + accent, + subtitle, + rightSlot, +}: { + /** Numero romano grande (es. "II", "IV", "VII") usato come art di sfondo. */ + chapter: string; + /** Eyebrow in alto. */ + eyebrow: string; + /** Titolo principale. */ + title: string; + /** Parola/parole da evidenziare in gradient italico (sostituisce nel titolo). */ + accent?: string; + /** Sottotitolo italic, indentato. */ + subtitle?: string; + /** Slot opzionale a destra del titolo (es. CTA "rigenera", date pickers). */ + rightSlot?: React.ReactNode; +}) { + let titleNode: React.ReactNode = title; + if (accent) { + const idx = title.toLowerCase().indexOf(accent.toLowerCase()); + if (idx >= 0) { + titleNode = ( + <> + {title.slice(0, idx)} + {title.slice(idx, idx + accent.length)} + {title.slice(idx + accent.length)} + + ); + } + } + + return ( +
    + {/* Chapter marker decorativo */} + + {chapter} + + +
    +
    +

    {eyebrow}

    +

    + {titleNode} + . +

    + {subtitle ? ( +

    + {subtitle} +

    + ) : null} +
    + {rightSlot ? ( +
    + {rightSlot} +
    + ) : null} +
    +
    + ); +} diff --git a/apps/web/src/styles/globals.css b/apps/web/src/styles/globals.css index fda9533..58e3e12 100644 --- a/apps/web/src/styles/globals.css +++ b/apps/web/src/styles/globals.css @@ -160,4 +160,258 @@ .monogram { @apply font-display text-2xl italic leading-none tracking-tight; } + + /* ───────────────────────────────────────────────────────────────────── + * Atmosfera moderna: blob, gradient, motion utilities. + * Usate dalla landing rifatta — pensate per essere applicate con + * parsimonia (tutta la palette è "warm cream" di base). + * ─────────────────────────────────────────────────────────────────── */ + + .mesh-blob { + position: absolute; + border-radius: 9999px; + filter: blur(64px); + opacity: 0.55; + pointer-events: none; + will-change: transform; + } + + .mesh-blob--oliva { + background: radial-gradient(closest-side, hsl(var(--oliva) / 0.7), transparent); + } + .mesh-blob--pomodoro { + background: radial-gradient(closest-side, hsl(var(--pomodoro) / 0.6), transparent); + } + .mesh-blob--oro { + background: radial-gradient(closest-side, hsl(var(--oro) / 0.55), transparent); + } + + .text-gradient { + background: linear-gradient( + 120deg, + hsl(var(--pomodoro)) 0%, + hsl(var(--oro)) 45%, + hsl(var(--oliva)) 100% + ); + background-size: 200% 200%; + -webkit-background-clip: text; + background-clip: text; + color: transparent; + animation: gradient-pan 9s ease-in-out infinite; + } + + .hover-lift { + transition: + transform 320ms cubic-bezier(0.2, 0.7, 0.2, 1), + box-shadow 320ms cubic-bezier(0.2, 0.7, 0.2, 1), + border-color 320ms ease; + } + .hover-lift:hover { + transform: translateY(-4px); + box-shadow: + 0 14px 30px -18px hsl(var(--ink) / 0.25), + 0 2px 4px -2px hsl(var(--ink) / 0.06); + } + + .glow-ring { + box-shadow: + 0 0 0 1px hsl(var(--ink) / 0.06), + 0 18px 40px -22px hsl(var(--pomodoro) / 0.45), + 0 6px 18px -10px hsl(var(--oliva) / 0.25); + } + + .btn-gradient { + background: linear-gradient( + 120deg, + hsl(var(--pomodoro)) 0%, + hsl(var(--oro)) 50%, + hsl(var(--oliva)) 100% + ); + background-size: 180% 180%; + color: hsl(var(--carta-light)); + transition: + background-position 600ms ease, + transform 200ms ease, + box-shadow 320ms ease; + box-shadow: + 0 12px 30px -14px hsl(var(--pomodoro) / 0.55), + 0 4px 10px -4px hsl(var(--oliva) / 0.3); + } + .btn-gradient:hover { + background-position: 100% 50%; + transform: translateY(-1px); + } + .btn-gradient:active { + transform: translateY(1px); + } + + /* Display tipografico WOW: stroke outline (text-only border). Funziona + * con -webkit-text-stroke che ha buon supporto su tutti i browser + * moderni; il fill resta trasparente così si vede solo il contorno. */ + .text-stroke { + -webkit-text-stroke: 2px hsl(var(--ink)); + color: transparent; + } + .text-stroke-thick { + -webkit-text-stroke: 3px hsl(var(--ink)); + color: transparent; + } + + /* Sezione dark per il contrasto col cream — usata nel CTA finale. + * Niente paper grain qui, palette inverted. */ + .section-dark { + background: linear-gradient(135deg, hsl(28 18% 8%) 0%, hsl(8 30% 14%) 100%); + color: hsl(var(--carta)); + } + + /* Mockup card 3D — leggera rotazione X+Y per dare profondità. + * Il container va settato perspective-1000 per attivare la prospettiva. */ + .perspective-1000 { + perspective: 1400px; + } + .tilt-3d { + transform: rotateX(8deg) rotateY(-12deg) rotateZ(0.5deg); + transform-style: preserve-3d; + transition: transform 600ms cubic-bezier(0.2, 0.7, 0.2, 1); + } + .tilt-3d:hover { + transform: rotateX(2deg) rotateY(-3deg) rotateZ(0deg); + } +} + +@layer utilities { + /* Marquee: scroll orizzontale infinito per keyword strip. + * Il contenuto deve essere duplicato due volte per evitare il salto. */ + @keyframes marquee { + from { + transform: translateX(0); + } + to { + transform: translateX(-50%); + } + } + .animate-marquee { + animation: marquee 38s linear infinite; + } + .animate-marquee--fast { + animation: marquee 22s linear infinite; + } + .animate-marquee--reverse { + animation: marquee 38s linear infinite reverse; + } + + /* Display gigante WOW per il titolo hero — fino a 16vw. */ + .text-mega { + font-size: clamp(3.5rem, 16vw, 12rem); + line-height: 0.85; + letter-spacing: -0.045em; + } + + /* Chapter marker gigante usato come art di sfondo per le sezioni. */ + .text-chapter { + font-size: clamp(8rem, 28vw, 22rem); + line-height: 0.8; + letter-spacing: -0.06em; + } + + /* Stroke fine, per chapter markers di sfondo. */ + .text-stroke-thin { + -webkit-text-stroke: 1.5px hsl(var(--ink) / 0.18); + color: transparent; + } + + /* Bleed: rompe il container per attaccarsi al bordo del viewport. + * Usato sui titoli mega che devono "uscire dalla pagina". */ + .bleed-left { + margin-left: calc(50% - 50vw); + padding-left: max(1.5rem, calc(50vw - 50%)); + } + .bleed-right { + margin-right: calc(50% - 50vw); + padding-right: max(1.5rem, calc(50vw - 50%)); + } + + @media (prefers-reduced-motion: reduce) { + .animate-marquee, + .animate-marquee--fast, + .animate-marquee--reverse { + animation: none; + } + } +} + +@layer utilities { + @keyframes gradient-pan { + 0%, + 100% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + } + + @keyframes float-x { + 0%, + 100% { + transform: translate3d(0, 0, 0); + } + 50% { + transform: translate3d(2.5rem, -1.5rem, 0); + } + } + + @keyframes float-y { + 0%, + 100% { + transform: translate3d(0, 0, 0); + } + 50% { + transform: translate3d(-1.5rem, 2.5rem, 0); + } + } + + @keyframes float-z { + 0%, + 100% { + transform: translate3d(0, 0, 0) scale(1); + } + 50% { + transform: translate3d(1.5rem, 1.5rem, 0) scale(1.08); + } + } + + .animate-float-x { + animation: float-x 16s ease-in-out infinite; + } + .animate-float-y { + animation: float-y 20s ease-in-out infinite; + } + .animate-float-z { + animation: float-z 24s ease-in-out infinite; + } + + @keyframes ring-trace { + 0% { + stroke-dashoffset: 360; + } + 100% { + stroke-dashoffset: 0; + } + } + + .animate-ring-trace { + animation: ring-trace 2.6s cubic-bezier(0.7, 0, 0.3, 1) both; + } + + /* Reduce motion: stoppa i drift in sottofondo. */ + @media (prefers-reduced-motion: reduce) { + .animate-float-x, + .animate-float-y, + .animate-float-z, + .text-gradient, + .animate-ring-trace { + animation: none; + } + } }