v3: follows, notifications, invite-only registration, unread markers
This commit is contained in:
@@ -1,13 +1,18 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { SubmitEvent } from "react";
|
||||
import { Link, useNavigate } from "react-router";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router";
|
||||
|
||||
import { API_URL } from "../config/api.ts";
|
||||
import { deserializeAuthResponse } from "../model.ts";
|
||||
import { useAuth } from "../hooks/useAuth.ts";
|
||||
import { PageShell } from "../components/PageShell.tsx";
|
||||
|
||||
type UserRegisterState =
|
||||
type TokenState =
|
||||
| { status: "checking" }
|
||||
| { status: "invalid" }
|
||||
| { status: "valid" };
|
||||
|
||||
type FormState =
|
||||
| { status: "idle" }
|
||||
| { status: "submitting" }
|
||||
| { status: "error"; error: string };
|
||||
@@ -15,13 +20,29 @@ type UserRegisterState =
|
||||
export function UserRegister() {
|
||||
const navigate = useNavigate();
|
||||
const { login } = useAuth();
|
||||
const [searchParams] = useSearchParams();
|
||||
const token = searchParams.get("token") ?? "";
|
||||
|
||||
const [state, setState] = useState<UserRegisterState>({ status: "idle" });
|
||||
const [tokenState, setTokenState] = useState<TokenState>({
|
||||
status: "checking",
|
||||
});
|
||||
const [formState, setFormState] = useState<FormState>({ status: "idle" });
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
setTokenState({ status: "invalid" });
|
||||
return;
|
||||
}
|
||||
fetch(`${API_URL}/api/invites/${encodeURIComponent(token)}`)
|
||||
.then((r) => {
|
||||
setTokenState(r.ok ? { status: "valid" } : { status: "invalid" });
|
||||
})
|
||||
.catch(() => setTokenState({ status: "invalid" }));
|
||||
}, [token]);
|
||||
|
||||
const handleSubmit = async (e: SubmitEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
setState({ status: "submitting" });
|
||||
setFormState({ status: "submitting" });
|
||||
|
||||
const formData = new FormData(e.currentTarget);
|
||||
const username = formData.get("username");
|
||||
@@ -31,34 +52,56 @@ export function UserRegister() {
|
||||
const res = await fetch(`${API_URL}/api/users/register`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ username, password }),
|
||||
body: JSON.stringify({ username, password, inviteToken: token }),
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const apiResponse = await res.json();
|
||||
|
||||
if (apiResponse.success) {
|
||||
login(deserializeAuthResponse(apiResponse.data));
|
||||
navigate("/");
|
||||
} else {
|
||||
setState({ status: "error", error: apiResponse.error.message });
|
||||
setFormState({
|
||||
status: "error",
|
||||
error: apiResponse.error?.message ?? "Registration failed.",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
setState({
|
||||
setFormState({
|
||||
status: "error",
|
||||
error: err instanceof Error ? err.message : "Registration failed.",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (tokenState.status === "checking") {
|
||||
return (
|
||||
<PageShell centered>
|
||||
<p className="page-loading">Checking invite…</p>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
if (tokenState.status === "invalid") {
|
||||
return (
|
||||
<PageShell centered>
|
||||
<div className="auth-card">
|
||||
<h1 className="auth-card-title">Invalid invite</h1>
|
||||
<p className="auth-card-footer">
|
||||
This invite link is missing, expired, or already used.
|
||||
</p>
|
||||
</div>
|
||||
</PageShell>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PageShell centered>
|
||||
<div className="auth-card">
|
||||
<h1 className="auth-card-title">Register</h1>
|
||||
|
||||
{state.status === "error" && (
|
||||
<div className="error-banner">{state.error}</div>
|
||||
{formState.status === "error" && (
|
||||
<div className="error-banner">{formState.error}</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="auth-form">
|
||||
@@ -67,7 +110,7 @@ export function UserRegister() {
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
required
|
||||
disabled={state.status === "submitting"}
|
||||
disabled={formState.status === "submitting"}
|
||||
autoFocus
|
||||
/>
|
||||
<input
|
||||
@@ -75,14 +118,14 @@ export function UserRegister() {
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
required
|
||||
disabled={state.status === "submitting"}
|
||||
disabled={formState.status === "submitting"}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary"
|
||||
disabled={state.status === "submitting"}
|
||||
disabled={formState.status === "submitting"}
|
||||
>
|
||||
{state.status === "submitting" ? "Registering…" : "Register"}
|
||||
{formState.status === "submitting" ? "Registering…" : "Register"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user