SPB Git forge

spb/fetcha

Public
11commits 1branches 0releases
1.5 MBsize
maindefault branch
16 days agolast push
TypeScript 97.5% SQL 1.4% Python 0.8%
6.4 KB · 137 lines tsx
Raw Blame History
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