feat: das Dashboard -- und die eine Regel steht im Code, nicht im Vorsatz
Schritt 4. React 18, TypeScript, Vite 5, Tailwind 4 (CSS-first), Lucide, Zustand; kein Konsta. Quelle in server/ui, Build nach server/web (gitignored), im Abbild von einer eigenen Docker-Stufe gebaut. JEDER KNOPF GIBT RUECKMELDUNG -- und zwar erzwungen, nicht vorgenommen: - api.ts haelt den EINZIGEN fetch und GENAU EINE Fehlerklasse. Der Fivemanage-Fehler war, zwei zu haben: geworfen wurde `new Error`, geprueft auf `instanceof ApiError`, und damit verschwand jeder Fehlschlag lautlos. - store.ts haelt run(): Aktion rein, Erfolgstext rein, und es meldet bei Erfolg diesen und bei Fehlschlag den Text DES DIENSTES. Wer darueber geht, kann keinen stillen Knopf bauen. - Jeder wartende Knopf ist gesperrt und zeigt einen Kreisel. Der zweite Klick eine Sekunde spaeter war der Weg zu neunzehn gleichnamigen Organisationen. - Fehlermeldungen bleiben stehen, bis jemand sie wegklickt. Ein Fehler, der von allein verschwindet, ist einer, den niemand gelesen hat. @vitejs/plugin-react ist auf ^4 festgenagelt: die 6 verlangt vite ^8. In den peerDependencies nachgesehen -- Vite 5.4.21 liegt in der Schnittmenge von plugin-react@4 und @tailwindcss/vite@4. Am laufenden Container nachgemessen: / kommt als text/html, assets/*.js als text/javascript, *.css als text/css (die MIME-Falle aus Schritt 1 traegt), und /tokens, /speicher, /konto liefern dieselbe index.html -- die SPA-Rueckfall- route tut, wofuer sie gebaut wurde. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { HardDrive, Images, KeyRound, LogOut, PieChart, UserCog } from 'lucide-react'
|
||||
import { run, useStore } from './store'
|
||||
import { Anmelden } from './pages/Anmelden'
|
||||
import { Galerie } from './pages/Galerie'
|
||||
import { Speicher } from './pages/Speicher'
|
||||
import { Tokens } from './pages/Tokens'
|
||||
import { Konto } from './pages/Konto'
|
||||
import { Meldungen } from './components/Meldungen'
|
||||
|
||||
// Ein winziger Router über die History-API statt einer Bibliothek.
|
||||
//
|
||||
// Echte Pfade und keine Reiter im Zustand, weil der Dienst dafür schon gebaut
|
||||
// ist: index.ts liefert auf alles Unbekannte die index.html aus, damit ein
|
||||
// Neuladen auf /tokens im Router landet und nicht im 404.
|
||||
|
||||
const seiten = [
|
||||
{ pfad: '/', name: 'Galerie', kind: Images, Inhalt: Galerie },
|
||||
{ pfad: '/speicher', name: 'Speicher', kind: PieChart, Inhalt: Speicher },
|
||||
{ pfad: '/tokens', name: 'Token', kind: KeyRound, Inhalt: Tokens },
|
||||
{ pfad: '/konto', name: 'Konto', kind: UserCog, Inhalt: Konto },
|
||||
] as const
|
||||
|
||||
function usePfad(): [string, (p: string) => void] {
|
||||
const [pfad, setPfad] = useState(window.location.pathname)
|
||||
|
||||
useEffect(() => {
|
||||
const zurueck = () => setPfad(window.location.pathname)
|
||||
window.addEventListener('popstate', zurueck)
|
||||
return () => window.removeEventListener('popstate', zurueck)
|
||||
}, [])
|
||||
|
||||
return [
|
||||
pfad,
|
||||
(p: string) => {
|
||||
window.history.pushState(null, '', p)
|
||||
setPfad(p)
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export function App() {
|
||||
// Einzeln ausgewählt und nicht als ganzer Laden: `useStore()` ohne
|
||||
// Auswahlfunktion abonniert ALLES, und dann zeichnet jede Meldung das
|
||||
// ganze Dashboard neu — bei 60 Kacheln mit Bildern ist das spürbar.
|
||||
const user = useStore((s) => s.user)
|
||||
const checking = useStore((s) => s.checking)
|
||||
const checkSession = useStore((s) => s.checkSession)
|
||||
const logout = useStore((s) => s.logout)
|
||||
const [pfad, gehZu] = usePfad()
|
||||
|
||||
useEffect(() => {
|
||||
void checkSession()
|
||||
}, [checkSession])
|
||||
|
||||
// Solange nicht feststeht, ob eine Sitzung besteht, wird NICHTS gezeigt.
|
||||
// Sonst blitzt bei jedem Neuladen kurz das Anmeldeformular auf, obwohl man
|
||||
// angemeldet ist — und das sieht aus wie ein Fehler.
|
||||
if (checking) {
|
||||
return (
|
||||
<div className="flex min-h-full items-center justify-center">
|
||||
<HardDrive className="animate-pulse text-schrift-leise" />
|
||||
<Meldungen />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<>
|
||||
<Anmelden />
|
||||
<Meldungen />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const aktiv = seiten.find((s) => s.pfad === pfad) ?? seiten[0]
|
||||
const Inhalt = aktiv.Inhalt
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full flex-col">
|
||||
<header className="sticky top-0 z-40 border-b border-rand bg-flaeche/80 backdrop-blur">
|
||||
<div className="mx-auto flex max-w-7xl flex-wrap items-center gap-x-6 gap-y-2 px-4 py-2.5">
|
||||
<a
|
||||
href="/"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
gehZu('/')
|
||||
}}
|
||||
className="flex items-center gap-2 font-semibold"
|
||||
>
|
||||
<HardDrive size={18} className="text-akzent" />
|
||||
d4rk_media
|
||||
</a>
|
||||
|
||||
<nav className="flex flex-1 gap-1">
|
||||
{seiten.map((s) => {
|
||||
const an = s.pfad === aktiv.pfad
|
||||
return (
|
||||
<a
|
||||
key={s.pfad}
|
||||
href={s.pfad}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
gehZu(s.pfad)
|
||||
}}
|
||||
className={`flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm
|
||||
transition-colors ${
|
||||
an
|
||||
? 'bg-flaeche-hoch text-schrift'
|
||||
: 'text-schrift-leise hover:text-schrift'
|
||||
}`}
|
||||
>
|
||||
<s.kind size={15} />
|
||||
{s.name}
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
|
||||
<button
|
||||
onClick={() => void run(logout, 'Abgemeldet.')}
|
||||
className="flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-sm
|
||||
text-schrift-leise hover:text-schrift"
|
||||
>
|
||||
<LogOut size={15} />
|
||||
Abmelden
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="mx-auto w-full max-w-7xl flex-1 px-4 py-5">
|
||||
<Inhalt />
|
||||
</main>
|
||||
|
||||
<Meldungen />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user