/* ========================================================================== ACCOUNT: login, registration, public profile, settings. ========================================================================== */ function AuthForm({ mode }) { const { go } = useRouter(); const { setUser } = useAuth(); const notify = useToast(); const register = mode === "register"; const [form, setForm] = useState({ username: "", email: "", password: "" }); const [accepted, setAccepted] = useState(false); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value })); async function submit(e) { e.preventDefault(); setBusy(true); setError(""); try { const user = await BB.post( register ? "/auth/register" : "/auth/login", register ? { ...form, acceptedTerms: accepted } : form ); setUser(user); notify(register ? `Willkommen, ${user.displayName}!` : `Willkommen zurück, ${user.displayName}.`); const next = new URLSearchParams(window.location.search).get("next"); go(next || "/"); } catch (err) { setError(err.message); setBusy(false); } } return (
{register ? "Profil erstellen" : "Willkommen zurück"}

{register ? "Werde Teil der Bible." : "Einloggen"}

{register && ( )} {register && ( )} {error &&
⚠ {error}
}
{register ? ( <>{BB.t("Schon dabei?")} {BB.t("Einloggen")} ) : ( <> {BB.t("Noch kein Profil?")} {BB.t("Jetzt erstellen")}
{BB.t("Die Redaktion setzt dein Passwort auf Anfrage zurück. Schreib uns dazu kurz über die im Impressum genannte Adresse.")}
)}
); } /* --- neues Passwort ueber einen Reset-Link -------------------------------- */ /* Die Gegenseite zu "Reset-Link erzeugen" in der Verwaltung. Der Link wird zuerst geprueft, bevor ein Formular erscheint: ein abgelaufenes Token erst nach dem Ausfuellen zu melden, waere die Arbeit umsonst. */ function PasswortNeu({ params }) { const { go } = useRouter(); const { setUser } = useAuth(); const notify = useToast(); const token = (params.get("token") || "").trim(); const [stand, setStand] = useState("pruefe"); // pruefe | offen | ungueltig const [konto, setKonto] = useState(""); const [form, setForm] = useState({ password: "", wieder: "" }); const [error, setError] = useState(""); const [busy, setBusy] = useState(false); useEffect(() => { let alive = true; if (!token) { setStand("ungueltig"); return; } BB.get("/auth/passwort-neu" + BB.qs({ token })) .then((res) => { if (!alive) return; setStand(res.gueltig ? "offen" : "ungueltig"); setKonto(res.username || ""); }) .catch(() => alive && setStand("ungueltig")); return () => { alive = false; }; }, [token]); const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value })); async function submit(e) { e.preventDefault(); if (form.password !== form.wieder) { setError(BB.t("Die beiden Passwörter stimmen nicht überein.")); return; } setBusy(true); setError(""); try { const user = await BB.post("/auth/passwort-neu", { token, password: form.password }); setUser(user); notify(BB.t("Dein Passwort ist gesetzt. Du bist jetzt eingeloggt."), "ok"); go("/"); } catch (err) { setError(err.message); setBusy(false); } } if (stand === "pruefe") { return
; } if (stand === "ungueltig") { return (
{BB.t("Neues Passwort setzen")}

{BB.t("Dieser Link gilt nicht mehr")}

{BB.t("Der Link ist abgelaufen oder wurde schon benutzt. Bitte fordere einen neuen an.")}

{BB.t("Zur Anmeldung")}
); } return (
{BB.t("Neues Passwort setzen")}

{BB.t("Neues Passwort")}

{konto &&

{BB.t("Konto:")} @{konto}

}
{error &&
⚠ {error}
}
); } /* --- public profile ------------------------------------------------------ */ function Profile({ username }) { const { user } = useAuth(); const [profile, setProfile] = useState(null); const [missing, setMissing] = useState(false); const [kind, setKind] = useState(""); useEffect(() => { let alive = true; setProfile(null); setMissing(false); BB.get("/users/" + encodeURIComponent(username)) .then((p) => { if (alive) setProfile(p); }) .catch(() => { if (alive) setMissing(true); }); return () => { alive = false; }; }, [username]); const { items, total, loading } = usePosts({ author: username, kind: kind || [].concat(BB.USER_KINDS, "wiki").join(","), sort: "new", }); if (missing) { return (
{BB.t("Zum Forum")}} />
); } if (!profile) return
; const mine = user && user.id === profile.id; const posts = Object.values(profile.counts || {}).reduce((a, b) => a + b, 0); return (

{profile.displayName}

@{profile.username} · dabei seit {BB.when(profile.createdAt)}
{profile.bio &&

{profile.bio}

}
{BB.num(posts)}
{BB.t("Beiträge")}
{BB.num(profile.karma)}
{BB.t("Upvotes")}
{Object.entries(profile.stats || {}).filter(([, v]) => v).map(([k, v]) => (
{v}
{k}
))}
{mine && {BB.t("Profil bearbeiten")}}
({ value: k, label: `${BB.KINDS[k] ? BB.KINDS[k].plural : k} ${profile.counts[k]}`, })) )} /> {BB.num(total)} veröffentlicht
{loading && items.length === 0 ? ( ) : items.length === 0 ? ( {BB.t("Ersten Beitrag schreiben")} : null} /> ) : (
{items.map((p) => )}
)}
); } /* --- settings ------------------------------------------------------------ */ const STAT_FIELDS = [ { key: "Größe", placeholder: "184 cm" }, { key: "Gewicht", placeholder: "92 kg" }, { key: "Jahre", placeholder: "8" }, { key: "Split", placeholder: "PPL" }, ]; function Settings() { const { user, setUser, ready } = useAuth(); const { go } = useRouter(); const notify = useToast(); const [form, setForm] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); useEffect(() => { if (ready && !user) go("/login?next=/einstellungen"); if (user && !form) { setForm({ bio: user.bio, avatar: user.avatar, stats: user.stats || {} }); } }, [ready, user, form, go]); if (!ready || !form) return
; const set = (k) => (e) => setForm((f) => ({ ...f, [k]: e.target.value })); async function submit(e) { e.preventDefault(); setBusy(true); setError(""); try { const updated = await BB.patch("/me", form); setUser(updated); notify("Profil gespeichert"); go("/u/" + updated.username); } catch (err) { setError(err.message); setBusy(false); } } async function logout() { await BB.post("/auth/logout").catch(() => {}); setUser(null); notify("Abgemeldet"); go("/"); } // Zahlungsmittel, Rechnungen und Kuendigung laufen ueber Stripes eigenes // Portal. Das selbst zu bauen hiesse, ein Rechnungsarchiv und eine // Kuendigungsstrecke nachzubauen, und beides muss rechtlich stimmen. async function verwalten() { setBusy(true); try { const { url } = await BB.post("/zahlung/verwalten"); window.location.href = url; } catch (err) { notify(err.message, "bad"); setBusy(false); } } return (
{BB.t("Einstellungen")}

{BB.t("Dein Profil")}

setForm((f) => ({ ...f, avatar }))} />