first implementation
This commit is contained in:
parent
79458bcba4
commit
870804e7b9
24 changed files with 5485 additions and 184 deletions
37
frontend/app/signup/[code]/page.tsx
Normal file
37
frontend/app/signup/[code]/page.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter, useParams } from "next/navigation";
|
||||
import { useAuth } from "../../auth-context";
|
||||
|
||||
export default function SignupWithCodePage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const { user } = useAuth();
|
||||
const code = params.code as string;
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
// Already logged in, redirect to home
|
||||
router.replace("/");
|
||||
} else {
|
||||
// Redirect to signup with code as query param
|
||||
router.replace(`/signup?code=${encodeURIComponent(code)}`);
|
||||
}
|
||||
}, [user, code, router]);
|
||||
|
||||
return (
|
||||
<main style={{
|
||||
minHeight: "100vh",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #0f0f23 100%)",
|
||||
color: "rgba(255,255,255,0.6)",
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
}}>
|
||||
Redirecting...
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1,20 +1,85 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState, useEffect, 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";
|
||||
|
||||
export default function SignupPage() {
|
||||
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 [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const { register } = useAuth();
|
||||
|
||||
const { user, register } = useAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
// Redirect if already logged in
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
router.push("/");
|
||||
}
|
||||
}, [user, router]);
|
||||
|
||||
// Check invite code on mount if provided in URL
|
||||
useEffect(() => {
|
||||
if (initialCode) {
|
||||
checkInvite(initialCode);
|
||||
}
|
||||
}, [initialCode]);
|
||||
|
||||
const checkInvite = 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);
|
||||
}
|
||||
};
|
||||
|
||||
const handleInviteSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
checkInvite(inviteCode);
|
||||
};
|
||||
|
||||
const handleSignupSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
|
||||
|
|
@ -31,7 +96,7 @@ export default function SignupPage() {
|
|||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
await register(email, password);
|
||||
await register(email, password, inviteCode.trim());
|
||||
router.push("/");
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Registration failed");
|
||||
|
|
@ -40,16 +105,88 @@ export default function SignupPage() {
|
|||
}
|
||||
};
|
||||
|
||||
// Show loading or redirect if user is already logged in
|
||||
if (user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 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}>Get started with your journey</p>
|
||||
<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={handleSubmit} style={styles.form}>
|
||||
<form onSubmit={handleSignupSubmit} style={styles.form}>
|
||||
{error && <div style={styles.error}>{error}</div>}
|
||||
|
||||
<div style={styles.field}>
|
||||
|
|
@ -62,6 +199,7 @@ export default function SignupPage() {
|
|||
style={styles.input}
|
||||
placeholder="you@example.com"
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -104,13 +242,42 @@ export default function SignupPage() {
|
|||
</form>
|
||||
|
||||
<p style={styles.footer}>
|
||||
Already have an account?{" "}
|
||||
<a href="/login" style={styles.link}>
|
||||
Sign in
|
||||
</a>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue