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:
Stefan Lazic committed 2026-08-02 17:47:23 +01:00
1 parent 468d163299
commit 090646fdfd
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>
);
}