Files
better-auth/demo/nextjs/components/sign-up.tsx
2025-09-03 13:03:54 -07:00

211 lines
5.6 KiB
TypeScript

"use client";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { useState, useTransition } from "react";
import { Loader2, X } from "lucide-react";
import { signUp } from "@/lib/auth-client";
import { toast } from "sonner";
import { useSearchParams, useRouter } from "next/navigation";
import Link from "next/link";
import { getCallbackURL } from "@/lib/shared";
export function SignUp() {
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [passwordConfirmation, setPasswordConfirmation] = useState("");
const [image, setImage] = useState<File | null>(null);
const [imagePreview, setImagePreview] = useState<string | null>(null);
const router = useRouter();
const params = useSearchParams();
const [loading, startTransition] = useTransition();
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
setImage(file);
setImagePreview((preview) => {
if (preview) {
URL.revokeObjectURL(preview);
}
return URL.createObjectURL(file);
});
}
};
return (
<Card className="z-50 rounded-md rounded-t-none max-w-md">
<CardHeader>
<CardTitle className="text-lg md:text-xl">Sign Up</CardTitle>
<CardDescription className="text-xs md:text-sm">
Enter your information to create an account
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4">
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="first-name">First name</Label>
<Input
id="first-name"
placeholder="Max"
required
onChange={(e) => {
setFirstName(e.target.value);
}}
value={firstName}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="last-name">Last name</Label>
<Input
id="last-name"
placeholder="Robinson"
required
onChange={(e) => {
setLastName(e.target.value);
}}
value={lastName}
/>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
placeholder="m@example.com"
required
onChange={(e) => {
setEmail(e.target.value);
}}
value={email}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
autoComplete="new-password"
placeholder="Password"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="password">Confirm Password</Label>
<Input
id="password_confirmation"
type="password"
value={passwordConfirmation}
onChange={(e) => setPasswordConfirmation(e.target.value)}
autoComplete="new-password"
placeholder="Confirm Password"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="image">Profile Image (optional)</Label>
<div className="flex items-end gap-4">
{imagePreview && (
<div className="relative w-16 h-16 rounded-sm overflow-hidden">
<img
src={imagePreview}
alt="Profile preview"
className="object-cover w-full h-full"
/>
</div>
)}
<div className="flex items-center gap-2 w-full">
<Input
id="image"
type="file"
accept="image/*"
onChange={handleImageChange}
className="w-full"
/>
{imagePreview && (
<X
className="cursor-pointer"
onClick={() => {
setImage(null);
setImagePreview(null);
}}
/>
)}
</div>
</div>
</div>
<Button
type="submit"
className="w-full"
disabled={loading}
onClick={async () => {
startTransition(async () => {
await signUp.email({
email,
password,
name: `${firstName} ${lastName}`,
image: image ? await convertImageToBase64(image) : "",
callbackURL: "/dashboard",
fetchOptions: {
onError: (ctx) => {
toast.error(ctx.error.message);
},
onSuccess: async () => {
toast.success("Successfully signed up");
router.push(getCallbackURL(params));
},
},
});
});
}}
>
{loading ? (
<Loader2 size={16} className="animate-spin" />
) : (
"Create an account"
)}
</Button>
</div>
</CardContent>
<CardFooter>
<div className="flex justify-center w-full border-t pt-4">
<p className="text-center text-xs text-neutral-500">
built with{" "}
<Link
href="https://better-auth.com"
className="underline"
target="_blank"
>
<span className="dark:text-white/70 cursor-pointer">
better-auth.
</span>
</Link>
</p>
</div>
</CardFooter>
</Card>
);
}
async function convertImageToBase64(file: File): Promise<string> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
}