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:
1 parent
221b567d45
commit
a5d8dc355d
10 files changed
+133
-17
No files matched your search
@@ -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>.",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
) : (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user