import { create } from 'zustand' import { api, ApiError, type User } from './api' // -- Rückmeldungen ------------------------------------------------------------- // // DIE EINE REGEL: jeder Knopf gibt Rückmeldung, auch der erfolgreiche. Deshalb // steht die Meldungsliste hier im Laden und nicht in irgendeiner Seite — sie // gehört zu jeder von ihnen. export type NoteKind = 'ok' | 'fehler' | 'hinweis' export type Note = { id: number; kind: NoteKind; text: string } let nextNoteId = 1 type State = { user: User | null /** Solange true, weiß noch niemand, ob eine Sitzung besteht — in dieser * Zeit darf weder das Anmeldeformular noch das Dashboard aufblitzen. */ checking: boolean notes: Note[] /** Wie viele Sitzungen dieses Konto offen hat. 0 = noch nicht geladen. */ sitzungen: number checkSession: () => Promise login: (username: string, password: string) => Promise logout: () => Promise note: (kind: NoteKind, text: string) => void dismiss: (id: number) => void } export const useStore = create((set, get) => ({ user: null, checking: true, sitzungen: 0, notes: [], async checkSession() { try { const { user, sitzungen } = await api.me() set({ user, sitzungen: sitzungen ?? 0, checking: false }) } catch (err) { // 401 ist hier kein Fehler, sondern die Antwort "noch nicht // angemeldet". Alles andere ist einer und wird gesagt. if (err instanceof ApiError && err.status === 401) { set({ user: null, checking: false }) return } set({ user: null, checking: false }) get().note('fehler', err instanceof Error ? err.message : 'Unbekannter Fehler') } }, async login(username, password) { const { user } = await api.login(username, password) set({ user }) get().note('ok', `Angemeldet als ${user.username}.`) }, async logout() { await api.logout() set({ user: null }) // Die Erfolgsmeldung kommt hier NICHT: der Aufrufer geht über `run`, // und der meldet. Beides hieße zwei Zettel für einen Klick. }, note(kind, text) { const id = nextNoteId++ set((s) => ({ notes: [...s.notes, { id, kind, text }] })) // Fehler bleiben stehen, bis jemand sie wegklickt. Ein Fehler, der von // allein verschwindet, ist ein Fehler, den niemand gelesen hat. if (kind !== 'fehler') { setTimeout(() => get().dismiss(id), 4000) } }, dismiss(id) { set((s) => ({ notes: s.notes.filter((n) => n.id !== id) })) }, })) /** Eine Aktion ausführen und IMMER etwas dazu sagen. * * Der Erfolgstext wird mitgegeben, der Fehlertext kommt vom Dienst. Wer * darüber geht, kann keinen stillen Knopf bauen — und genau darum geht es. */ export async function run( action: () => Promise, erfolg: string | ((result: T) => string), ): Promise { const { note } = useStore.getState() try { const result = await action() note('ok', typeof erfolg === 'function' ? erfolg(result) : erfolg) return result } catch (err) { note('fehler', err instanceof Error ? err.message : 'Unbekannter Fehler') // Eine abgelaufene Sitzung ist kein gewöhnlicher Fehler: sie führt // zurück aufs Anmeldeformular, statt jeden weiteren Knopf mit // "nicht angemeldet" zu quittieren. if (err instanceof ApiError && err.status === 401) { useStore.setState({ user: null }) } return null } }