Neuer Hook useScrollRestoration (einmal im AppShell gemountet) erhält die Scrollposition je Route über: - Zurück/Vorwärts-Navigation (POP restauriert, PUSH startet oben), - Bildschirm-Sperre / App-Hintergrund (visibilitychange, pagehide/pageshow) — damit das Handy nach dem Entsperren nicht mehr oben landet. Robust gegen nachladende Listen (rAF-Nachjustierung bis ~2,5 s, während die Liste wächst), bricht bei eigener Scroll-Eingabe ab, Deep-Links (?focus=, #anchor) haben Vorrang. Die bisher nur in der Tickets-Seite eingebaute Logik wurde durch den globalen Hook ersetzt. tsc/eslint/vitest grün; e2e (navigation/listen/tickets, 42) grün. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
104 lines
4.0 KiB
TypeScript
104 lines
4.0 KiB
TypeScript
import { useState } from 'react'
|
|
import type { ReactNode } from 'react'
|
|
import { NavLink, Outlet } from 'react-router-dom'
|
|
import { de } from '../strings/de'
|
|
import GerbilIcon from './GerbilIcon'
|
|
import { BreederSuffixProvider } from './BreederSuffixProvider'
|
|
import { useScrollRestoration } from '../hooks/useScrollRestoration'
|
|
import './appShell.css'
|
|
|
|
interface NavItem {
|
|
to: string
|
|
label: string
|
|
icon: ReactNode
|
|
end?: boolean
|
|
}
|
|
|
|
// Primary items are always visible in the phone bottom bar; secondary items
|
|
// move into the "Mehr" sheet on phones and appear inline in the desktop sidebar.
|
|
const PRIMARY: NavItem[] = [
|
|
{ to: '/rennmaeuse', label: de.nav.gerbils, icon: <GerbilIcon size="1.25em" /> },
|
|
{ to: '/wuerfe', label: de.nav.litters, icon: '🍼' },
|
|
{ to: '/genetik', label: de.nav.genetics, icon: '🧬' },
|
|
]
|
|
const SECONDARY: NavItem[] = [
|
|
{ to: '/gehege', label: de.nav.enclosures, icon: '🏜️' },
|
|
{ to: '/kontakte', label: de.nav.contacts, icon: '📇' },
|
|
{ to: '/abgabe', label: de.nav.forSale, icon: '🏡' },
|
|
// ABGABE-STATUS: Reservierungs-/Abgabe-Status
|
|
{ to: '/reservierungen', label: de.nav.reservations, icon: '🔖' },
|
|
// INBOX-1: Anfragen-Posteingang
|
|
{ to: '/anfragen', label: de.nav.requests, icon: '📨' },
|
|
// WAITLIST: Warteliste/Nachfrage (RennmausPro nachfrage_tb)
|
|
{ to: '/warteliste', label: de.nav.waitingList, icon: '📝' },
|
|
// RÜCKNAHMEN: zurückgekommene/zurückgenommene Tiere
|
|
{ to: '/ruecknahmen', label: de.nav.returns, icon: '↩️' },
|
|
{ to: '/statistik', label: de.nav.statistics, icon: '📊' },
|
|
// FEAT-13: Abgabeverträge + Zuchtprofil
|
|
{ to: '/vertraege', label: de.nav.contracts, icon: '📄' },
|
|
{ to: '/einstellungen', label: de.nav.settings, icon: '⚙️' },
|
|
// WEB-0b: CMS-Verwaltung der öffentlichen Webseite
|
|
{ to: '/webseite', label: de.nav.website, icon: '🌐' },
|
|
{ to: '/farbkatalog', label: de.nav.colorCatalog, icon: '🎨' },
|
|
{ to: '/hilfe', label: de.nav.help, icon: '❓' },
|
|
]
|
|
|
|
const linkClass = ({ isActive }: { isActive: boolean }) =>
|
|
isActive ? 'nav-link nav-link--active' : 'nav-link'
|
|
|
|
/**
|
|
* App-Grundgerüst, mobile-first:
|
|
* - Smartphone: Kopfzeile oben, Tab-Leiste unten (4 Haupt-Tabs + „Mehr“).
|
|
* - Laptop (ab 768px): Seitenleiste links mit allen Einträgen, kein „Mehr“.
|
|
*/
|
|
export default function AppShell() {
|
|
const [moreOpen, setMoreOpen] = useState(false)
|
|
// Scrollposition je Seite erhalten (Zurück-Navigation + Handy-Sperre/Hintergrund) — gilt für
|
|
// ALLE scrollbaren Listen der App.
|
|
useScrollRestoration()
|
|
|
|
return (
|
|
<div className="app-shell">
|
|
<nav className="app-nav" aria-label={de.nav.mainNavigation}>
|
|
<div className="nav-primary">
|
|
{PRIMARY.map((item) => (
|
|
<NavLink key={item.to} to={item.to} end={item.end} className={linkClass}>
|
|
<span className="nav-icon" aria-hidden="true">{item.icon}</span>
|
|
<span className="nav-label">{item.label}</span>
|
|
</NavLink>
|
|
))}
|
|
<button
|
|
type="button"
|
|
className={moreOpen ? 'nav-link nav-more-btn nav-link--active' : 'nav-link nav-more-btn'}
|
|
aria-expanded={moreOpen}
|
|
onClick={() => setMoreOpen((o) => !o)}
|
|
>
|
|
<span className="nav-icon" aria-hidden="true">☰</span>
|
|
<span className="nav-label">{de.nav.more}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div className={moreOpen ? 'nav-secondary nav-secondary--open' : 'nav-secondary'}>
|
|
{SECONDARY.map((item) => (
|
|
<NavLink
|
|
key={item.to}
|
|
to={item.to}
|
|
className={linkClass}
|
|
onClick={() => setMoreOpen(false)}
|
|
>
|
|
<span className="nav-icon" aria-hidden="true">{item.icon}</span>
|
|
<span className="nav-label">{item.label}</span>
|
|
</NavLink>
|
|
))}
|
|
</div>
|
|
</nav>
|
|
|
|
<main className="app-main">
|
|
<BreederSuffixProvider>
|
|
<Outlet />
|
|
</BreederSuffixProvider>
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|