TypeScript 97.5%
SQL 1.4%
Python 0.8%
1"use client";2import * as React from "react";3import Link from "next/link";4import { useRouter } from "next/navigation";5import { MailCheck } from "lucide-react";6import { authClient } from "@/lib/auth-client";7import { recordLegalAcceptance } from "@/actions/legal";8import { AuthCard } from "@/components/auth/auth-card";9import { PasswordInput, PasswordRequirements, passwordStrong } from "@/components/auth/password-input";10import { Button } from "@/components/ui/button";11import { Input } from "@/components/ui/input";12import { Field, FieldError, Label } from "@/components/ui/label";13import { Alert } from "@/components/ui/alert";1415const ACCESS_DENIED = "This email is not on the access list. Ask your Fetcha administrator to invite you.";1617export function SignupForm({ next, initialEmail }: { next?: string; initialEmail?: string }) {18 const router = useRouter();19 const [name, setName] = React.useState("");20 const [email, setEmail] = React.useState(initialEmail ?? "");21 const [password, setPassword] = React.useState("");22 const [confirm, setConfirm] = React.useState("");23 const [agree, setAgree] = React.useState(false);24 const [error, setError] = React.useState<string | null>(null);25 const [loading, setLoading] = React.useState(false);26 const [done, setDone] = React.useState(false);2728 const mismatch = confirm.length > 0 && confirm !== password;2930 async function onSubmit(e: React.FormEvent) {31 e.preventDefault();32 setError(null);33 if (!passwordStrong(password)) return setError("Choose a stronger password.");34 if (password !== confirm) return setError("Passwords do not match.");35 if (!agree) return setError("You must accept the Terms of Service and Privacy Policy.");36 setLoading(true);37 const { error } = await authClient.signUp.email({ name: name.trim() || email.split("@")[0]!, email: email.trim().toLowerCase(), password, callbackURL: "/dashboard?verified=1" });38 if (error) {39 setLoading(false);40 const msg = error.message ?? "";41 if (error.status === 403 || /access list/i.test(msg)) setError(ACCESS_DENIED);42 else if (error.status === 422 || /exist/i.test(msg)) setError("An account with this email already exists. Try logging in.");43 else setError(msg || "Could not create the account.");44 return;45 }46 await recordLegalAcceptance().catch(() => {});47 setLoading(false);48 setDone(true);49 setTimeout(() => {50 router.push(next && next.startsWith("/") ? next : "/dashboard");51 router.refresh();52 }, 2500);53 }5455 if (done) {56 return (57 <AuthCard title="Check your inbox" description={<>We sent a verification link to <strong className="text-fg">{email}</strong>. Verify it to unlock API access — the Playground is available right away.</>}>58 <div className="flex items-center gap-3 rounded-lg border border-border bg-bg-subtle p-4 text-[13.5px]">59 <MailCheck className="size-5 text-success" />60 <span>Redirecting you to the dashboard…</span>61 </div>62 <Button asChild className="mt-4 w-full" variant="outline">63 <Link href="/dashboard">Open the dashboard now</Link>64 </Button>65 </AuthCard>66 );67 }6869 return (70 <AuthCard71 title="Create your account"72 description={73 <>74 Fetcha is invitation-only. Use the address your administrator approved.{" "}75 <a href="mailto:hello@fetcha.co?subject=Fetcha%20access" className="text-fg underline-offset-4 hover:underline">76 Request access77 </a>{" "}78 if you have not been invited yet.79 </>80 }81 footer={82 <>83 Already have an account?{" "}84 <Link href="/login" className="font-medium text-fg underline-offset-4 hover:underline">85 Log in86 </Link>87 </>88 }89 >90 <form onSubmit={onSubmit} className="grid gap-4" noValidate>91 {error ? <Alert variant="danger">{error}</Alert> : null}92 <Field>93 <Label htmlFor="name">Name <span className="font-normal text-fg-subtle">(optional)</span></Label>94 <Input id="name" name="name" autoComplete="name" value={name} onChange={(e) => setName(e.target.value)} placeholder="Ada Lovelace" />95 </Field>96 <Field>97 <Label htmlFor="email">Approved email</Label>98 <Input id="email" name="email" type="email" autoComplete="email" required value={email} onChange={(e) => setEmail(e.target.value)} placeholder="you@company.com" readOnly={Boolean(initialEmail) && email === initialEmail} aria-describedby="email-hint" />99 <p id="email-hint" className="text-xs text-fg-subtle">100 {initialEmail && email === initialEmail ? (101 <>102 Pre-filled from your invitation.{" "}103 <button type="button" className="underline-offset-4 hover:underline" onClick={() => setEmail("")}>104 Use another address105 </button>106 </>107 ) : (108 "Must match the address on the access list exactly."109 )}110 </p>111 </Field>112 <Field>113 <Label htmlFor="password">Password</Label>114 <PasswordInput id="password" name="password" autoComplete="new-password" required value={password} onChange={(e) => setPassword(e.target.value)} />115 <PasswordRequirements password={password} />116 </Field>117 <Field>118 <Label htmlFor="confirm">Confirm password</Label>119 <PasswordInput id="confirm" name="confirm" autoComplete="new-password" required value={confirm} onChange={(e) => setConfirm(e.target.value)} aria-invalid={mismatch || undefined} />120 <FieldError>{mismatch ? "Passwords do not match." : null}</FieldError>121 </Field>122 <label className="flex items-start gap-2.5 text-[13px] text-fg-muted leading-snug">123 <input type="checkbox" checked={agree} onChange={(e) => setAgree(e.target.checked)} className="mt-0.5 size-4 shrink-0 rounded border-border accent-[var(--accent)]" required />124 <span>125 I agree to the{" "}126 <Link href="/legal/terms" target="_blank" className="text-fg underline-offset-4 hover:underline">Terms of Service</Link> and{" "}127 <Link href="/legal/privacy" target="_blank" className="text-fg underline-offset-4 hover:underline">Privacy Policy</Link>.128 </span>129 </label>130 <Button type="submit" size="lg" loading={loading} className="mt-1 w-full" disabled={!agree}>131 Create account132 </Button>133 </form>134 </AuthCard>135 );136}137