/* ==========================================================================
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 (
);
}
/* --- 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 (
);
}
/* --- 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]) => (
))}
{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}
/>
) : (
)}
);
}
/* --- 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")}
{/* Eine erteilte Einwilligung muss jederzeit widerrufbar sein, und zwar
so einfach, wie sie erteilt wurde. Deshalb steht sie hier und nicht
hinter einem Link in der Datenschutzerklaerung. */}
);
}
Object.assign(window, { AuthForm, PasswortNeu, Profile, Settings });