From ca65ddcacc76bf2a8d14adb6a69080dc4be8b8ca Mon Sep 17 00:00:00 2001 From: irrlicht Date: Sat, 26 Sep 2026 23:38:03 +0200 Subject: [PATCH] React-Frontend: Login, Registrierung, Header, neuer Beitrag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthBox mit Login/Registrierung (Auto-Login nach Registrierung) - Compose mit Zwischenablage-Bild, Feed springt danach an den Anfang - Headerbar mit Profil-Link und Logout - useAuthChanged: me und Votes nach Login/Logout neu laden - kein Aufblitzen der Login-Box, solange der Anmeldestatus lädt Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01TiXsPUqw7oeomZ8wZrQW5q --- frontend/src/Layout.tsx | 23 ++++++++- frontend/src/auth.ts | 14 +++++- frontend/src/components/AuthBox.tsx | 73 +++++++++++++++++++++++++++++ frontend/src/components/Compose.tsx | 45 ++++++++++++++++++ frontend/src/pages/FeedPage.tsx | 14 +++++- 5 files changed, 165 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/AuthBox.tsx create mode 100644 frontend/src/components/Compose.tsx diff --git a/frontend/src/Layout.tsx b/frontend/src/Layout.tsx index 53a8418..bf1813f 100644 --- a/frontend/src/Layout.tsx +++ b/frontend/src/Layout.tsx @@ -1,4 +1,6 @@ import { Link, Outlet, useLocation } from "react-router"; +import { api } from "./api"; +import { useAuthChanged, useMe } from "./auth"; // Tage ohne Datenverlust (fester Startpunkt, Kontinuität seit der alten // Version). @@ -30,7 +32,7 @@ export function Layout() {

Ohne Datenverlust seit {plural(days, "Tag", "Tagen")}

-
+ ) : (
zurück zum Feed
@@ -43,3 +45,22 @@ export function Layout() { ); } + +// Headerbar der Startseite: angemeldet Profil-Link und Logout, sonst leer. +function Headerbar() { + const me = useMe().data; + const authChanged = useAuthChanged(); + if (!me) return
; + + // POST statt GET: als GET wäre Logout per fremdem Link auslösbar (CSRF). + const logout = async () => { + await api.post("/auth/logout").catch(() => undefined); + await authChanged(); + }; + return ( +
+ mein Profil{" "} + +
+ ); +} diff --git a/frontend/src/auth.ts b/frontend/src/auth.ts index 9e4c541..bd15319 100644 --- a/frontend/src/auth.ts +++ b/frontend/src/auth.ts @@ -1,4 +1,4 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { ApiError, api } from "./api"; import type { Me } from "./types"; @@ -19,3 +19,15 @@ export function useMe() { }, }); } + +// useAuthChanged liefert die Aktion nach Login/Logout: alles, was vom +// Anmeldestatus abhängt, neu laden -- das eigene Konto und die eigene Auswahl +// bei den Votes. +export function useAuthChanged() { + const qc = useQueryClient(); + return () => + Promise.all([ + qc.invalidateQueries({ queryKey: meKey }), + qc.invalidateQueries({ queryKey: ["votes"] }), + ]); +} diff --git a/frontend/src/components/AuthBox.tsx b/frontend/src/components/AuthBox.tsx new file mode 100644 index 0000000..49c233f --- /dev/null +++ b/frontend/src/components/AuthBox.tsx @@ -0,0 +1,73 @@ +import { useState, type FormEvent, type MouseEvent } from "react"; +import { api, errMsg } from "../api"; +import { useAuthChanged } from "../auth"; + +// AuthBox: Login und Registrierung im Wechsel. Die {" "} bilden die +// Leerzeichen des alten Markups nach -- Überschrift, Inputs und Knöpfe sind +// inline, der Abstand zwischen ihnen ist also sichtbar. +export function AuthBox() { + const [mode, setMode] = useState<"login" | "register">("login"); + const [msg, setMsg] = useState(""); + const [busy, setBusy] = useState(false); + const authChanged = useAuthChanged(); + + const show = (which: "login" | "register") => (e: MouseEvent) => { + e.preventDefault(); + setMode(which); + setMsg(""); + }; + + // submit sperrt das Formular für die Dauer des Requests und zeigt einen + // Fehler als Meldung; action wirft bei Misserfolg (ApiError). + const submit = (action: (fd: FormData) => Promise, fallback: string) => + async (e: FormEvent) => { + e.preventDefault(); + setBusy(true); + try { + await action(new FormData(e.currentTarget)); + setMsg(""); + await authChanged(); + } catch (err) { + setMsg(errMsg(err, fallback)); + } finally { + setBusy(false); + } + }; + + const login = submit((fd) => api.post("/auth/login", fd), "Login fehlgeschlagen."); + + // Nach der Registrierung direkt mit denselben Daten einloggen, damit der + // Nutzer gleich drin ist. newuser meldet alle Feldfehler auf einmal. + const register = submit(async (fd) => { + await api.post("/auth/newuser", fd); + const creds = new FormData(); + creds.append("user", String(fd.get("user"))); + creds.append("pass", String(fd.get("pass1"))); + creds.append("timeout", "86400"); + await api.post("/auth/login", creds); + }, "Registrierung fehlgeschlagen."); + + return ( +
+ + + + +

{msg}

+
+ ); +} diff --git a/frontend/src/components/Compose.tsx b/frontend/src/components/Compose.tsx new file mode 100644 index 0000000..5f23cd6 --- /dev/null +++ b/frontend/src/components/Compose.tsx @@ -0,0 +1,45 @@ +import { useState, type FormEvent } from "react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { api, errMsg } from "../api"; +import { pasteImage } from "../paste"; +import type { CreatedEntry } from "../types"; + +// Compose: neuer Beitrag. Nach Erfolg springt der Feed zurück auf Seite 0, +// damit der eigene Beitrag oben steht. +export function Compose() { + const qc = useQueryClient(); + const [pasteMsg, setPasteMsg] = useState(""); + const create = useMutation({ + mutationFn: (fd: FormData) => api.post("/entry/create", fd), + }); + + const onSubmit = (e: FormEvent) => { + e.preventDefault(); + const form = e.currentTarget; + create.mutate(new FormData(form), { + onSuccess: () => { + form.reset(); + setPasteMsg(""); + qc.resetQueries({ queryKey: ["feed"] }); + }, + }); + }; + + return ( +
+
{ + if (pasteImage(e)) setPasteMsg("Bild aus Zwischenablage übernommen."); + }} + > + {" "} +

{pasteMsg}

{" "} + {" "} + +
+

{create.isError ? errMsg(create.error, "Beitrag fehlgeschlagen.") : ""}

+
+ ); +} diff --git a/frontend/src/pages/FeedPage.tsx b/frontend/src/pages/FeedPage.tsx index 9658aca..ece080c 100644 --- a/frontend/src/pages/FeedPage.tsx +++ b/frontend/src/pages/FeedPage.tsx @@ -1,6 +1,16 @@ +import { useMe } from "../auth"; +import { AuthBox } from "../components/AuthBox"; +import { Compose } from "../components/Compose"; import { Feed } from "../components/Feed"; -// Login/Registrierung und das Formular für neue Beiträge folgen in Schritt 4. +// Solange der Anmeldestatus noch lädt, weder Login noch Formular zeigen -- +// sonst blitzt bei Angemeldeten kurz die Login-Box auf. export function FeedPage() { - return `/entry/feed/${page}`} />; + const me = useMe(); + return ( + <> + {me.isSuccess && (me.data ? : )} + `/entry/feed/${page}`} /> + + ); }