feat(frontend): Oberfläche mit RRULE-Editor und Logo-Auswahl
- Vite, React 18, TypeScript und Tailwind mit dunklem Standard-Theme über CSS-Variablen, heller Modus umschaltbar und in localStorage gemerkt - Anmeldung, erzwungener Passwortwechsel, Layout mit Seitenleiste - API-Client mit stiller Token-Erneuerung, TanStack Query mit Fehler-Toasts - Seiten Recurrences (inkl. Detail-Drawer), Transactions, Merchants, Settings - Geführter RRULE-Editor mit Vorlagen, Expertenmodus, deutschem Klartext und Live-Vorschau der nächsten Termine vom preview-Endpunkt - Firmen-Kachelgrid mit Logo, Markenfarbe und Logo-Auswahldialog samt Upload - Beträge durchgängig in de-DE, Eingabe in beiden Schreibweisen - 49 Tests, tsc --noEmit und eslint sauber, Produktionsbundle baut Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
/** Anmeldung, Abmeldung und der angemeldete Benutzer. */
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { ApiError, api, isUnauthorized } from "@/lib/api";
|
||||
import { keys } from "@/lib/queryClient";
|
||||
import { toast } from "@/store/toast";
|
||||
import type { MessageResponse, User } from "@/types/api";
|
||||
|
||||
export function useMe() {
|
||||
return useQuery({
|
||||
queryKey: keys.me,
|
||||
queryFn: async () => {
|
||||
try {
|
||||
return await api.get<User>("/me");
|
||||
} catch (fehler) {
|
||||
// Kein gültiges Token bedeutet schlicht: nicht angemeldet.
|
||||
if (isUnauthorized(fehler)) return null;
|
||||
throw fehler;
|
||||
}
|
||||
},
|
||||
staleTime: 60_000,
|
||||
retry: false,
|
||||
});
|
||||
}
|
||||
|
||||
export function useLogin() {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (daten: { username: string; password: string }) =>
|
||||
api.post<User>("/auth/login", daten),
|
||||
onSuccess: (benutzer) => {
|
||||
client.setQueryData(keys.me, benutzer);
|
||||
void client.invalidateQueries();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useLogout() {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: () => api.post<MessageResponse>("/auth/logout"),
|
||||
onSuccess: () => {
|
||||
client.setQueryData(keys.me, null);
|
||||
client.clear();
|
||||
toast.info("Abgemeldet.");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useChangePassword() {
|
||||
const client = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (daten: { current_password: string; new_password: string }) =>
|
||||
api.post<MessageResponse>("/auth/change-password", daten),
|
||||
onSuccess: () => {
|
||||
// Der Passwortwechsel beendet alle Sitzungen – es folgt eine neue Anmeldung.
|
||||
client.setQueryData(keys.me, null);
|
||||
client.clear();
|
||||
toast.success("Passwort geändert.", "Bitte melde dich neu an.");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Übersetzt einen Anmeldefehler in eine Meldung für das Formular. */
|
||||
export function loginErrorMessage(error: unknown): string {
|
||||
if (error instanceof ApiError) return error.message;
|
||||
return "Der Server ist nicht erreichbar.";
|
||||
}
|
||||
Reference in New Issue
Block a user