Phase 2
Base app architecture:
Route groups for customer, staff and owner.
* Reusable design system with around 20 UI components.
* Complete Prisma schema.
* Authentication with email/password and Google.
* Role and permission model.
* Protected routes.
* Clean separation between features, services, components and shared utilities.
This commit is contained in:
201 files changed
+31984
No files matched your search
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { registerAction } from "@/features/auth/actions/register";
|
||||
import { registerSchema, type RegisterInput } from "@/features/auth/schemas";
|
||||
|
||||
export function RegisterForm() {
|
||||
const [formError, setFormError] = useState<string | null>(null);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setError,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<RegisterInput>({
|
||||
resolver: zodResolver(registerSchema),
|
||||
});
|
||||
|
||||
const onSubmit = async (data: RegisterInput) => {
|
||||
setFormError(null);
|
||||
const formData = new FormData();
|
||||
Object.entries(data).forEach(([key, value]) => formData.set(key, value ?? ""));
|
||||
|
||||
const result = await registerAction(formData);
|
||||
|
||||
// A successful call redirects server-side and never resolves here with
|
||||
// success: true in practice, but we still handle it for completeness.
|
||||
if (!result.success) {
|
||||
setFormError(result.error ?? "Something went wrong. Please try again.");
|
||||
if (result.fieldErrors) {
|
||||
for (const [field, messages] of Object.entries(result.fieldErrors)) {
|
||||
if (messages?.[0]) {
|
||||
setError(field as keyof RegisterInput, { message: messages[0] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} noValidate className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Input label="First Name" placeholder="Alex" error={errors.firstName?.message} {...register("firstName")} />
|
||||
<Input label="Last Name" placeholder="Chen" error={errors.lastName?.message} {...register("lastName")} />
|
||||
</div>
|
||||
<Input
|
||||
label="Email Address"
|
||||
type="email"
|
||||
placeholder="name@example.com"
|
||||
error={errors.email?.message}
|
||||
{...register("email")}
|
||||
/>
|
||||
<Input
|
||||
label="Phone Number (optional)"
|
||||
type="tel"
|
||||
placeholder="+44 7700 900000"
|
||||
error={errors.phone?.message}
|
||||
{...register("phone")}
|
||||
/>
|
||||
<Input
|
||||
label="Password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
error={errors.password?.message}
|
||||
{...register("password")}
|
||||
/>
|
||||
<Input
|
||||
label="Confirm Password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
error={errors.confirmPassword?.message}
|
||||
{...register("confirmPassword")}
|
||||
/>
|
||||
|
||||
{formError && (
|
||||
<p role="alert" className="text-sm font-medium text-red-500">
|
||||
{formError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="px-1 text-[10px] leading-relaxed text-muted">
|
||||
By creating an account, you agree to our <a href="#" className="underline">Terms of Service</a> and{" "}
|
||||
<a href="#" className="underline">Privacy Policy</a>.
|
||||
</p>
|
||||
|
||||
<Button type="submit" className="mt-4 w-full" isLoading={isSubmitting}>
|
||||
Sign Up
|
||||
</Button>
|
||||
|
||||
<p className="mt-10 text-center text-sm text-muted">
|
||||
Already have an account?{" "}
|
||||
<Link href="/sign-in" className="border-b border-ink font-bold text-ink">
|
||||
Log in
|
||||
</Link>
|
||||
</p>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user