Initial commit of Vellum, an event photo product for guest uploads, host moderation, and original-quality galleries.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-07 19:36:14 -04:00
co-authored by Cursor
commit 27e2f196eb
149 changed files with 13847 additions and 0 deletions
+137
View File
@@ -0,0 +1,137 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { UserPlusIcon } from "lucide-react";
import { authClient } from "@/lib/auth-client";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/spinner";
export function SignUpForm({
callbackURL,
requireInvite,
initialCode = "",
}: {
callbackURL: string;
requireInvite: boolean;
initialCode?: string;
}) {
const router = useRouter();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [inviteCode, setInviteCode] = useState(initialCode);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
async function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (requireInvite && !inviteCode.trim()) {
setError("An invite code is required");
return;
}
setLoading(true);
setError(null);
const result = await authClient.signUp.email({
name,
email,
password,
callbackURL,
});
if (result.error) {
setError(result.error.message ?? "Unable to create account");
setLoading(false);
return;
}
if (inviteCode.trim()) {
router.push(`/invitations/${encodeURIComponent(inviteCode.trim())}`);
router.refresh();
return;
}
router.push(callbackURL);
router.refresh();
}
return (
<form className="flex flex-col gap-5" onSubmit={submit}>
<FieldGroup>
<Field>
<FieldLabel htmlFor="name">Name</FieldLabel>
<Input
id="name"
autoComplete="name"
required
value={name}
onChange={(event) => setName(event.target.value)}
className="tap-target"
/>
</Field>
<Field>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input
id="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(event) => setEmail(event.target.value)}
className="tap-target"
/>
</Field>
<Field>
<FieldLabel htmlFor="password">Password</FieldLabel>
<Input
id="password"
type="password"
autoComplete="new-password"
minLength={process.env.NODE_ENV === "production" ? 10 : 5}
required
value={password}
onChange={(event) => setPassword(event.target.value)}
className="tap-target"
/>
<FieldDescription>Use at least 10 characters in production.</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="invite">
Invite code {requireInvite ? "" : "(optional)"}
</FieldLabel>
<Input
id="invite"
value={inviteCode}
onChange={(event) => setInviteCode(event.target.value)}
placeholder="VELLUM-…"
className="tap-target"
required={requireInvite}
/>
</Field>
</FieldGroup>
{error ? (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
) : null}
<Button type="submit" size="lg" className="tap-target w-full" disabled={loading}>
{loading ? (
<Spinner data-icon="inline-start" />
) : (
<UserPlusIcon data-icon="inline-start" />
)}
Create account
</Button>
<p className="text-center text-sm text-muted-foreground">
Already have an account?{" "}
<Link
href={`/sign-in?callbackURL=${encodeURIComponent(callbackURL)}`}
className="font-medium text-primary hover:underline"
>
Sign in
</Link>
</p>
</form>
);
}