Some checks failed
Deploy to Cloudflare Workers / deploy (push) Has been cancelled
222 lines
6.6 KiB
TypeScript
222 lines
6.6 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
import { useState } from "react";
|
|
import { createClient } from "@/lib/supabase/client";
|
|
import { formatAuthNetworkError } from "@/lib/supabase/env";
|
|
import { formatAuthError, formatSignupError } from "@/lib/supabase/auth-errors";
|
|
import { ensureUserProfile } from "@/lib/auth/resolve-portal-role";
|
|
import type { PortalRole } from "@/lib/auth/roles";
|
|
import { loginPathForRole, PORTAL_ROUTES } from "@/lib/auth/roles";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { PasswordInput } from "@/components/ui/password-input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from "@/components/ui/card";
|
|
|
|
async function signupViaApi(
|
|
email: string,
|
|
password: string,
|
|
displayName: string,
|
|
portalRole: PortalRole
|
|
) {
|
|
const res = await fetch("/api/auth/signup", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ email, password, displayName, portalRole }),
|
|
});
|
|
const json = (await res.json()) as {
|
|
success: boolean;
|
|
error?: string;
|
|
useClient?: boolean;
|
|
needsEmailConfirmation?: boolean;
|
|
};
|
|
return { ok: res.ok && json.success, status: res.status, ...json };
|
|
}
|
|
|
|
export function SignupForm({
|
|
portalRole,
|
|
title,
|
|
description,
|
|
}: {
|
|
portalRole: PortalRole;
|
|
title: string;
|
|
description: string;
|
|
}) {
|
|
const router = useRouter();
|
|
const [email, setEmail] = useState("");
|
|
const [password, setPassword] = useState("");
|
|
const [displayName, setDisplayName] = useState("");
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const loginHref = loginPathForRole(portalRole);
|
|
const portalLabel =
|
|
portalRole === "league_master" ? "League Master" : "Team Manager";
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const trimmedEmail = email.trim();
|
|
const api = await signupViaApi(
|
|
trimmedEmail,
|
|
password,
|
|
displayName,
|
|
portalRole
|
|
);
|
|
|
|
if (api.ok) {
|
|
if (api.needsEmailConfirmation) {
|
|
setError(
|
|
`Confirm your email, then sign in at ${portalLabel} login.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
const supabase = createClient();
|
|
const { error: signInError } = await supabase.auth.signInWithPassword({
|
|
email: trimmedEmail,
|
|
password,
|
|
});
|
|
if (signInError) {
|
|
setError(
|
|
`Account created. Sign in at ${loginHref} (${signInError.message})`
|
|
);
|
|
return;
|
|
}
|
|
router.push(PORTAL_ROUTES[portalRole]);
|
|
router.refresh();
|
|
return;
|
|
}
|
|
|
|
if (api.status !== 503 && !api.useClient) {
|
|
setError(formatSignupError(api.error ?? "Signup failed"));
|
|
return;
|
|
}
|
|
|
|
const supabase = createClient();
|
|
const { data, error: authError } = await supabase.auth.signUp({
|
|
email: trimmedEmail,
|
|
password,
|
|
options: {
|
|
data: { display_name: displayName, portal_role: portalRole },
|
|
},
|
|
});
|
|
|
|
if (authError) {
|
|
setError(formatAuthError(authError));
|
|
return;
|
|
}
|
|
|
|
if (data.user && !data.session) {
|
|
setError(
|
|
`Confirm your email, then sign in at ${portalLabel} login.`
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (data.user) {
|
|
const sync = await ensureUserProfile(
|
|
supabase,
|
|
data.user,
|
|
portalRole,
|
|
displayName
|
|
);
|
|
if (sync.error === "schema") {
|
|
setError(
|
|
"Account created but database needs an update. Run: npm run db:push — then sign in."
|
|
);
|
|
return;
|
|
}
|
|
if (!sync.ok && sync.error) {
|
|
setError(formatSignupError(sync.error));
|
|
return;
|
|
}
|
|
}
|
|
|
|
router.push(PORTAL_ROUTES[portalRole]);
|
|
router.refresh();
|
|
} catch (err) {
|
|
setError(formatAuthNetworkError(err));
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="retro-grid flex min-h-screen items-center justify-center px-4 py-12">
|
|
<Card className="retro-card w-full max-w-md border-border/80">
|
|
<CardHeader>
|
|
<p className="font-display text-[10px] font-bold uppercase tracking-[0.2em] text-neon">
|
|
Yaltopia FIFA
|
|
</p>
|
|
<CardTitle className="font-display text-xl font-black uppercase tracking-wide">
|
|
{title}
|
|
</CardTitle>
|
|
<CardDescription>{description}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label>Display name</Label>
|
|
<Input
|
|
value={displayName}
|
|
onChange={(e) => setDisplayName(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Email</Label>
|
|
<Input
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
required
|
|
autoComplete="email"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Password</Label>
|
|
<PasswordInput
|
|
value={password}
|
|
onChange={(e) => setPassword(e.target.value)}
|
|
required
|
|
minLength={6}
|
|
autoComplete="new-password"
|
|
/>
|
|
</div>
|
|
{error && (
|
|
<p className="whitespace-pre-line text-sm text-red-400">{error}</p>
|
|
)}
|
|
<Button type="submit" variant="neon" className="w-full" disabled={loading}>
|
|
{loading ? "Creating account…" : "Create account"}
|
|
</Button>
|
|
</form>
|
|
<p className="mt-4 text-center text-sm text-muted-foreground">
|
|
<Link href={loginHref} className="text-neon hover:underline">
|
|
Sign in
|
|
</Link>
|
|
{portalRole === "manager" && (
|
|
<>
|
|
{" · "}
|
|
<Link href="/" className="hover:underline">
|
|
Home
|
|
</Link>
|
|
</>
|
|
)}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|