feat(ui): nav app persistente nel masthead (desktop + drawer mobile)

L'utente non aveva modo di saltare da una pagina app all'altra senza tornare alla home (solo il logo era cliccabile). Aggiunta nav globale:

- **Desktop**: row di link orizzontale a destra del logo (Cucina · Tracking · Digiuno · Spesa · Profilo · Abbonamento). Pagina corrente evidenziata in pomodoro, hover sulle altre.
- **Mobile**: bottone hamburger (≡ Menu) che apre un drawer sotto la riga di header con le 6 voci. Si chiude da solo al cambio di pathname.

`<Masthead>` ora è un Client Component (serve usePathname). Prop `showNav` (default false) controlla la nav: attivata sulle 7 pagine logged-in (`/`, /plan, /tracking, /fasting, /shopping, /profile, /billing, /recipes/[id]). Lasciata off su welcome, onboarding, sign-in, sign-up.

Aggiunti i18n keys: navPlan/navTracking/navFasting/navShopping/navProfile/navBilling. Rimossi gli `issueLabel="N. XX — ..."" dalle pagine app (`issueLabel` resta supportato per pagine senza nav).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
lucianoandClaude Opus 4.7 committed 2026-05-07 15:29:43 +02:00
1 parent 221b567d45
commit a5d8dc355d
10 files changed
+133 -17

No files matched your search

+6
View File
@@ -16,6 +16,12 @@
"viewTracking": "Pesata & misure",
"viewFasting": "Digiuno intermittente",
"viewBilling": "Abbonamento & fatture",
"navPlan": "Cucina",
"navTracking": "Tracking",
"navFasting": "Digiuno",
"navShopping": "Spesa",
"navProfile": "Profilo",
"navBilling": "Abbonamento",
"lettura": "Continua la lettura",
"inizia": "Inizia",
"greeting": "Buongiorno, <name>{name}</name>.",
+2 -2
View File
@@ -39,7 +39,7 @@ function BillingPageContent({ status }: { status: BillingStatus | null }) {
return (
<div className="relative">
<div className="min-h-screen w-full px-6 sm:px-10 lg:px-16 xl:px-24">
<Masthead issueLabel="N. 08 — Abbonamento" />
<Masthead showNav />
<main className="pb-24 pt-12">
<p className="editorial-eyebrow">{t('errorLoading')}</p>
</main>
@@ -54,7 +54,7 @@ function BillingPageContent({ status }: { status: BillingStatus | null }) {
return (
<div className="relative">
<div className="min-h-screen w-full px-6 sm:px-10 lg:px-16 xl:px-24">
<Masthead issueLabel="N. 08 — Abbonamento" />
<Masthead showNav />
<main className="relative overflow-hidden pb-24 pt-10 sm:pt-12">
<CursorGlow color="hsl(var(--oro))" size={520} />
<div
+1 -1
View File
@@ -44,7 +44,7 @@ function FastingPageContent({
return (
<div className="relative">
<div className="min-h-screen w-full px-6 sm:px-10 lg:px-16 xl:px-24">
<Masthead issueLabel="N. 07 — Digiuno" />
<Masthead showNav />
<main className="relative overflow-hidden pb-24 pt-10 sm:pt-12">
<CursorGlow color="hsl(var(--pomodoro))" size={520} />
<div
+1 -1
View File
@@ -28,7 +28,7 @@ function HomeContent({
return (
<div className="relative">
<div className="min-h-screen w-full px-6 sm:px-10 lg:px-16 xl:px-24">
<Masthead />
<Masthead showNav={!!userName} />
{userName ? (
<SignedInDashboard userName={userName} billing={billing} />
) : (
+1 -1
View File
@@ -64,7 +64,7 @@ function PlanPageContent({
return (
<div className="relative">
<div className="min-h-screen w-full px-6 sm:px-10 lg:px-16 xl:px-24">
<Masthead issueLabel="N. 05 — Cucina" />
<Masthead showNav />
<main className="relative overflow-hidden py-10 sm:py-12">
<CursorGlow color="hsl(var(--pomodoro))" size={520} />
<div
+1 -1
View File
@@ -59,7 +59,7 @@ function ProfilePageContent({
return (
<div className="relative">
<div className="min-h-screen w-full px-6 sm:px-10 lg:px-16 xl:px-24">
<Masthead issueLabel="N. 04 — Profilo" />
<Masthead showNav />
<main className="relative overflow-hidden pb-24 pt-10 sm:pt-12">
<CursorGlow color="hsl(var(--oliva))" size={520} />
<div
@@ -111,7 +111,7 @@ function RecipeContent({ recipe }: { recipe: RecipeView }) {
return (
<div className="min-h-screen w-full px-6 sm:px-10 lg:px-16 xl:px-24">
<Masthead issueLabel={`N. 08 — ${t(`categoryLabel.${recipe.category}`)}`} />
<Masthead showNav />
<main className="py-10 sm:py-14">
<Link
href="/plan"
+1 -1
View File
@@ -54,7 +54,7 @@ function ShoppingPageContent({ list }: { list: ShoppingList | null }) {
return (
<div className="relative">
<div className="min-h-screen w-full px-6 sm:px-10 lg:px-16 xl:px-24">
<Masthead issueLabel="N. 09 — La spesa" />
<Masthead showNav />
<main className="relative overflow-hidden pb-24 pt-10 sm:pt-12">
<CursorGlow color="hsl(var(--oliva))" size={520} />
<div
+1 -1
View File
@@ -73,7 +73,7 @@ function TrackingPageContent({
return (
<div className="relative">
<div className="min-h-screen w-full px-6 sm:px-10 lg:px-16 xl:px-24">
<Masthead issueLabel="N. 06 — Tracking" />
<Masthead showNav />
<main className="relative overflow-hidden pb-24 pt-10 sm:pt-12">
<CursorGlow color="hsl(var(--oro))" size={520} />
<div
+118 -8
View File
@@ -1,33 +1,143 @@
'use client';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
import { cn } from '@/lib/utils';
interface MastheadProps {
/**
* Etichetta opzionale a destra (es. "N. 05 — Cucina"). Se omessa, l'header
* resta minimale con solo il logo a sinistra. Tono più \"app\" che \"magazine\".
* Etichetta opzionale a destra (es. "N. 05 — Cucina"). Mostrata solo
* quando la nav app non è attiva (auth pages, welcome, onboarding).
*/
issueLabel?: string;
/**
* Mostra la nav app (Cucina/Tracking/Digiuno/Spesa/Profilo/Abbonamento).
* Da accendere su tutte le pagine logged-in dopo onboarding.
*/
showNav?: boolean;
className?: string;
}
interface NavEntry {
href: string;
key: 'navPlan' | 'navTracking' | 'navFasting' | 'navShopping' | 'navProfile' | 'navBilling';
}
const NAV: ReadonlyArray<NavEntry> = [
{ href: '/plan', key: 'navPlan' },
{ href: '/tracking', key: 'navTracking' },
{ href: '/fasting', key: 'navFasting' },
{ href: '/shopping', key: 'navShopping' },
{ href: '/profile', key: 'navProfile' },
{ href: '/billing', key: 'navBilling' },
];
/**
* Top-of-page header minimale: logo a sinistra, sezione corrente a destra (se
* fornita). Una riga sottile sotto fa da divider editoriale.
* Top-of-page header dell'app. Logo a sx, nav orizzontale al centro/destra
* (desktop) o hamburger drawer (mobile). Pagina corrente evidenziata in
* pomodoro. Una riga sottile sotto fa da divider editoriale.
*/
export function Masthead({ issueLabel, className }: MastheadProps) {
export function Masthead({ issueLabel, showNav = false, className }: MastheadProps) {
const t = useTranslations('Home');
const pathname = usePathname();
const [open, setOpen] = useState(false);
// Quando il pathname cambia, chiudi il drawer mobile (utente ha navigato).
useEffect(() => setOpen(false), [pathname]);
// Match di fallback: il pathname è tipo /it/plan, dobbiamo confrontare
// ignorando il segmento di locale. Usiamo `.includes('/plan')` con
// controllo che inizi con /<locale>/plan oppure /plan.
function isActive(href: string): boolean {
if (!pathname) return false;
// strip locale prefix se presente
const stripped = pathname.replace(/^\/[a-z]{2}(\/|$)/, '/');
if (href === '/') return stripped === '/';
return stripped.startsWith(href);
}
return (
<header className={cn('relative pb-5 pt-6', className)}>
<div className="flex items-center justify-between gap-4">
<div className="flex items-center justify-between gap-6">
<Link
href="/"
className="monogram tracking-tight transition-opacity hover:opacity-70"
className="monogram shrink-0 tracking-tight transition-opacity hover:opacity-70"
aria-label="KetoPath"
>
KetoPath<span className="text-pomodoro">.</span>
</Link>
{issueLabel ? <p className="editorial-eyebrow hidden sm:block">{issueLabel}</p> : null}
{showNav ? (
<>
{/* Desktop nav */}
<nav className="hidden items-baseline gap-7 md:flex">
{NAV.map((item) => {
const active = isActive(item.href);
return (
<Link
key={item.href}
href={item.href}
className={cn(
'font-mono text-[11px] uppercase tracking-widest transition-colors',
active ? 'text-pomodoro' : 'text-ink-soft hover:text-ink',
)}
>
{t(item.key)}
</Link>
);
})}
</nav>
{/* Mobile: hamburger button */}
<button
type="button"
onClick={() => setOpen((v) => !v)}
aria-expanded={open}
aria-label="Apri menu"
className="text-ink hover:text-pomodoro -mr-2 inline-flex items-center gap-2 p-2 font-mono text-[11px] uppercase tracking-widest transition-colors md:hidden"
>
<span aria-hidden className="text-base leading-none">
{open ? '×' : '≡'}
</span>
<span>Menu</span>
</button>
</>
) : issueLabel ? (
<p className="editorial-eyebrow hidden sm:block">{issueLabel}</p>
) : null}
</div>
{/* Mobile drawer: appare sotto la riga di header */}
{showNav && open ? (
<nav
aria-label="Navigazione principale"
className="border-ink/15 bg-carta-light animate-fade-up mt-4 border-b border-t py-2 [animation-duration:200ms] md:hidden"
>
<ul className="flex flex-col">
{NAV.map((item) => {
const active = isActive(item.href);
return (
<li key={item.href}>
<Link
href={item.href}
className={cn(
'border-ink/10 flex items-center justify-between gap-4 border-b py-3 font-mono text-xs uppercase tracking-widest transition-colors last:border-b-0',
active ? 'text-pomodoro' : 'text-ink hover:text-pomodoro',
)}
>
{t(item.key)}
<span aria-hidden>→</span>
</Link>
</li>
);
})}
</ul>
</nav>
) : null}
<div className="rule mt-5" />
</header>
);