- Install prettier - Configure .prettierrc.json and .prettierignore - Add npm scripts: format, format:check - Add Makefile target: format-frontend - Format all frontend files
319 lines
8.9 KiB
TypeScript
319 lines
8.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback, Suspense } from "react";
|
|
import { useRouter, useSearchParams } from "next/navigation";
|
|
import { useAuth } from "../auth-context";
|
|
import { api } from "../api";
|
|
import { authFormStyles as styles } from "../styles/auth-form";
|
|
|
|
interface InviteCheckResponse {
|
|
valid: boolean;
|
|
status?: string;
|
|
error?: string;
|
|
}
|
|
|
|
function SignupContent() {
|
|
const searchParams = useSearchParams();
|
|
const initialCode = searchParams.get("code") || "";
|
|
|
|
const [inviteCode, setInviteCode] = useState(initialCode);
|
|
const [inviteValid, setInviteValid] = useState<boolean | null>(null);
|
|
const [inviteError, setInviteError] = useState("");
|
|
const [isCheckingInvite, setIsCheckingInvite] = useState(false);
|
|
const [isCheckingInitialCode, setIsCheckingInitialCode] = useState(!!initialCode);
|
|
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [confirmPassword, setConfirmPassword] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
|
|
const { user, register } = useAuth();
|
|
const router = useRouter();
|
|
|
|
// Redirect if already logged in
|
|
useEffect(() => {
|
|
if (user) {
|
|
router.push("/");
|
|
}
|
|
}, [user, router]);
|
|
|
|
const checkInvite = useCallback(async (code: string) => {
|
|
if (!code.trim()) {
|
|
setInviteValid(null);
|
|
setInviteError("");
|
|
return;
|
|
}
|
|
|
|
setIsCheckingInvite(true);
|
|
setInviteError("");
|
|
|
|
try {
|
|
const response = await api.get<InviteCheckResponse>(
|
|
`/api/invites/${encodeURIComponent(code.trim())}/check`
|
|
);
|
|
|
|
if (response.valid) {
|
|
setInviteValid(true);
|
|
setInviteError("");
|
|
} else {
|
|
setInviteValid(false);
|
|
setInviteError(response.error || "Invalid invite code");
|
|
}
|
|
} catch {
|
|
setInviteValid(false);
|
|
setInviteError("Failed to verify invite code");
|
|
} finally {
|
|
setIsCheckingInvite(false);
|
|
}
|
|
}, []);
|
|
|
|
// Check invite code on mount if provided in URL
|
|
useEffect(() => {
|
|
if (initialCode) {
|
|
checkInvite(initialCode).finally(() => setIsCheckingInitialCode(false));
|
|
}
|
|
}, [initialCode, checkInvite]);
|
|
|
|
const handleInviteSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
checkInvite(inviteCode);
|
|
};
|
|
|
|
const handleSignupSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setError("");
|
|
|
|
if (password !== confirmPassword) {
|
|
setError("Passwords do not match");
|
|
return;
|
|
}
|
|
|
|
if (password.length < 6) {
|
|
setError("Password must be at least 6 characters");
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
|
|
try {
|
|
await register(email, password, inviteCode.trim());
|
|
router.push("/");
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Registration failed");
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
// Show loading or redirect if user is already logged in
|
|
if (user) {
|
|
return null;
|
|
}
|
|
|
|
// Show loading state while checking initial code from URL
|
|
if (isCheckingInitialCode) {
|
|
return (
|
|
<main style={styles.main}>
|
|
<div style={styles.container}>
|
|
<div style={styles.card}>
|
|
<div style={{ textAlign: "center", color: "rgba(255,255,255,0.6)" }}>
|
|
Checking invite code...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
// Step 1: Enter invite code
|
|
if (!inviteValid) {
|
|
return (
|
|
<main style={styles.main}>
|
|
<div style={styles.container}>
|
|
<div style={styles.card}>
|
|
<div style={styles.header}>
|
|
<h1 style={styles.title}>Join with Invite</h1>
|
|
<p style={styles.subtitle}>Enter your invite code to get started</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleInviteSubmit} style={styles.form}>
|
|
{inviteError && <div style={styles.error}>{inviteError}</div>}
|
|
|
|
<div style={styles.field}>
|
|
<label htmlFor="inviteCode" style={styles.label}>
|
|
Invite Code
|
|
</label>
|
|
<input
|
|
id="inviteCode"
|
|
type="text"
|
|
value={inviteCode}
|
|
onChange={(e) => {
|
|
setInviteCode(e.target.value);
|
|
setInviteError("");
|
|
setInviteValid(null);
|
|
}}
|
|
style={styles.input}
|
|
placeholder="word-word-00"
|
|
required
|
|
autoFocus
|
|
/>
|
|
<span
|
|
style={{
|
|
...styles.link,
|
|
fontSize: "0.8rem",
|
|
marginTop: "0.5rem",
|
|
display: "block",
|
|
}}
|
|
>
|
|
Ask your inviter for this code
|
|
</span>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
style={{
|
|
...styles.button,
|
|
opacity: isCheckingInvite ? 0.7 : 1,
|
|
}}
|
|
disabled={isCheckingInvite || !inviteCode.trim()}
|
|
>
|
|
{isCheckingInvite ? "Checking..." : "Continue"}
|
|
</button>
|
|
</form>
|
|
|
|
<p style={styles.footer}>
|
|
Already have an account?{" "}
|
|
<a href="/login" style={styles.link}>
|
|
Sign in
|
|
</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
// Step 2: Enter email and password
|
|
return (
|
|
<main style={styles.main}>
|
|
<div style={styles.container}>
|
|
<div style={styles.card}>
|
|
<div style={styles.header}>
|
|
<h1 style={styles.title}>Create account</h1>
|
|
<p style={styles.subtitle}>
|
|
Using invite:{" "}
|
|
<code
|
|
style={{
|
|
background: "rgba(255,255,255,0.1)",
|
|
padding: "0.2rem 0.5rem",
|
|
borderRadius: "4px",
|
|
fontSize: "0.85rem",
|
|
}}
|
|
>
|
|
{inviteCode}
|
|
</code>
|
|
</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleSignupSubmit} style={styles.form}>
|
|
{error && <div style={styles.error}>{error}</div>}
|
|
|
|
<div style={styles.field}>
|
|
<label htmlFor="email" style={styles.label}>
|
|
Email
|
|
</label>
|
|
<input
|
|
id="email"
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
style={styles.input}
|
|
placeholder="you@example.com"
|
|
required
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
|
|
<div style={styles.field}>
|
|
<label htmlFor="password" style={styles.label}>
|
|
Password
|
|
</label>
|
|
<input
|
|
id="password"
|
|
type="password"
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
style={styles.input}
|
|
placeholder="••••••••"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div style={styles.field}>
|
|
<label htmlFor="confirmPassword" style={styles.label}>
|
|
Confirm Password
|
|
</label>
|
|
<input
|
|
id="confirmPassword"
|
|
type="password"
|
|
value={confirmPassword}
|
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
|
style={styles.input}
|
|
placeholder="••••••••"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
style={{
|
|
...styles.button,
|
|
opacity: isSubmitting ? 0.7 : 1,
|
|
}}
|
|
disabled={isSubmitting}
|
|
>
|
|
{isSubmitting ? "Creating account..." : "Create account"}
|
|
</button>
|
|
</form>
|
|
|
|
<p style={styles.footer}>
|
|
<button
|
|
onClick={() => {
|
|
setInviteValid(null);
|
|
setInviteError("");
|
|
}}
|
|
style={{
|
|
...styles.link,
|
|
background: "none",
|
|
border: "none",
|
|
cursor: "pointer",
|
|
padding: 0,
|
|
}}
|
|
>
|
|
Use a different invite code
|
|
</button>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
export default function SignupPage() {
|
|
return (
|
|
<Suspense
|
|
fallback={
|
|
<main style={styles.main}>
|
|
<div style={styles.container}>
|
|
<div style={styles.card}>
|
|
<div style={{ textAlign: "center", color: "rgba(255,255,255,0.6)" }}>Loading...</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
}
|
|
>
|
|
<SignupContent />
|
|
</Suspense>
|
|
);
|
|
}
|