Files
GerbilManager/gerbil-manager-web/src/components/AppShell.tsx
Gulum c20e6b5426 WEB-0b: CMS-Verwaltung der öffentlichen Webseite (Hand-Editier-UI)
Neuer Bereich /webseite im Manager (Nav: "Mehr"-Blatt) zur Pflege der 6
Webseiten-Seiten gegen Pams WEB-0-Backend (LAN-only, keine Auth):

- Übersicht: alle Seiten mit Status (Entwurf/Veröffentlicht).
- Seiten-Editor: Titel + SEO-Beschreibung + Status speichern.
- Geordneter Block-Editor: Blöcke hinzufügen (Überschrift, Textabschnitt/
  Markdown, Bild, Galerie, Kontaktangaben), bearbeiten, nach oben/unten
  verschieben (PUT .../blocks/order), löschen. Die dynamische AbgabetiereList
  ist nicht manuell anlegbar; nur ihr Einleitungstext ist editierbar.

Vertraege: GET /pages, GET /pages/{slug}, PUT /pages/{id},
POST /pages/{pageId}/blocks, PUT /blocks/{id}, DELETE /blocks/{id},
PUT /pages/{pageId}/blocks/order. Enums als Strings (Draft/Published, Heading...).

Deutsch via de.ts (LF beibehalten), mobile-first. e2e-Mock um die CMS-Routen
erweitert (zustandsbehaftet) + Smoke (Block anlegen/bearbeiten/verschieben,
bleibt erhalten). Gate gruen: build, eslint, vitest 69, e2e 68.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 11:23:34 +02:00

92 lines
3.2 KiB
TypeScript

import { useState } from 'react'
import { NavLink, Outlet } from 'react-router-dom'
import { de } from '../strings/de'
import './appShell.css'
interface NavItem {
to: string
label: string
icon: string
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: '/', label: de.nav.home, icon: '🏠', end: true },
{ to: '/rennmaeuse', label: de.nav.gerbils, icon: '🐭' },
{ to: '/wuerfe', label: de.nav.litters, icon: '🍼' },
{ to: '/genetik', label: de.nav.genetics, icon: '🧬' },
]
const SECONDARY: NavItem[] = [
{ to: '/becken', label: de.nav.enclosures, icon: '🛁' },
{ to: '/kontakte', label: de.nav.contacts, icon: '📇' },
{ to: '/abgabe', label: de.nav.forSale, 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: '/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)
return (
<div className="app-shell">
<header className="app-header">
<span className="app-logo" aria-hidden="true">🐭</span>
<h1 className="app-title">{de.app.title}</h1>
</header>
<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">
<Outlet />
</main>
</div>
)
}