- fix(seeder): add PASSWORD_PEPPER to all bcrypt hashes (admin + candidates/recruiters) - fix(seeder): add created_by field to jobs INSERT (was causing NOT NULL violation) - feat(backend): add custom job questions support in applications - feat(backend): add payment handler and Stripe routes - feat(frontend): add navbar and footer to /register and /register/user pages - feat(frontend): add custom question answers to job apply page - feat(frontend): update home page hero section and navbar buttons - feat(frontend): update auth/api lib with new endpoints - chore(db): add migration 045 for application answers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
353 lines
13 KiB
TypeScript
353 lines
13 KiB
TypeScript
"use client";
|
|
|
|
import { useMemo, useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import Link from "next/link";
|
|
import Image from "next/image";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
} from "@/components/ui/card";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import {
|
|
Briefcase,
|
|
AlertCircle,
|
|
Eye,
|
|
EyeOff,
|
|
User as UserIcon,
|
|
Lock,
|
|
Building2,
|
|
Mail,
|
|
Phone,
|
|
} from "lucide-react";
|
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
import { motion } from "framer-motion";
|
|
import { useForm } from "react-hook-form";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { z } from "zod";
|
|
import { Navbar } from "@/components/navbar";
|
|
import { Footer } from "@/components/footer";
|
|
|
|
type RegisterFormData = {
|
|
name: string;
|
|
email: string;
|
|
phone?: string;
|
|
password: string;
|
|
confirmPassword: string;
|
|
acceptTerms: boolean;
|
|
};
|
|
|
|
export default function RegisterUserPage() {
|
|
const router = useRouter();
|
|
const [error, setError] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
|
|
|
const registerSchema = useMemo(() => z.object({
|
|
name: z.string().min(3, "Name must be at least 3 characters"),
|
|
email: z.string().email("Invalid email address"),
|
|
phone: z.string().min(7, "Phone number too short").optional().or(z.literal("")),
|
|
password: z.string().min(6, "Password must be at least 6 characters"),
|
|
confirmPassword: z.string().min(6, "Please confirm your password"),
|
|
acceptTerms: z.boolean().refine((val) => val === true, {
|
|
message: "You must accept the terms of use",
|
|
}),
|
|
}).refine((data) => data.password === data.confirmPassword, {
|
|
message: "Passwords do not match",
|
|
path: ["confirmPassword"],
|
|
}), []);
|
|
|
|
const {
|
|
register,
|
|
handleSubmit,
|
|
formState: { errors },
|
|
} = useForm<RegisterFormData>({
|
|
resolver: zodResolver(registerSchema),
|
|
defaultValues: {
|
|
name: "",
|
|
email: "",
|
|
phone: "",
|
|
password: "",
|
|
confirmPassword: "",
|
|
acceptTerms: false,
|
|
},
|
|
});
|
|
|
|
const handleRegister = async (data: RegisterFormData) => {
|
|
setError("");
|
|
setLoading(true);
|
|
|
|
try {
|
|
const { registerCandidate } = await import("@/lib/auth");
|
|
|
|
await registerCandidate({
|
|
name: data.name,
|
|
email: data.email,
|
|
phone: data.phone || "",
|
|
password: data.password,
|
|
username: data.email.split("@")[0],
|
|
});
|
|
|
|
router.push("/login?message=Account created successfully! Please log in.");
|
|
} catch (err: any) {
|
|
setError(err.message || "Failed to create account. Please try again.");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen flex flex-col">
|
|
<Navbar />
|
|
|
|
<div className="flex-1 flex flex-col lg:flex-row">
|
|
{/* Left Side - Branding */}
|
|
<div className="lg:flex-1 bg-gradient-to-br from-primary to-primary/80 p-8 flex flex-col justify-center items-center text-primary-foreground">
|
|
<motion.div
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
className="max-w-md text-center"
|
|
>
|
|
<div className="flex items-center justify-center gap-3 mb-8">
|
|
<Image
|
|
src="/logohorse.png"
|
|
alt="GoHorseJobs"
|
|
width={140}
|
|
height={140}
|
|
className="rounded-lg"
|
|
/>
|
|
</div>
|
|
|
|
<h1 className="text-4xl font-bold mb-4">
|
|
Start your professional journey
|
|
</h1>
|
|
|
|
<p className="text-lg opacity-90 leading-relaxed">
|
|
Connect with the best opportunities worldwide. Sign up for free and find the job that's right for you!
|
|
</p>
|
|
|
|
<div className="mt-8 space-y-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center">
|
|
<UserIcon className="w-4 h-4" />
|
|
</div>
|
|
<span>Build your complete professional profile</span>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center">
|
|
<Building2 className="w-4 h-4" />
|
|
</div>
|
|
<span>Apply to top jobs from companies worldwide</span>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center">
|
|
<Briefcase className="w-4 h-4" />
|
|
</div>
|
|
<span>Track your applications in real time</span>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
|
|
{/* Right Side - Register Form */}
|
|
<div className="lg:flex-1 flex items-center justify-center p-8 bg-background">
|
|
<motion.div
|
|
initial={{ opacity: 0, x: 20 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
className="w-full max-w-md space-y-6"
|
|
>
|
|
<div className="text-center space-y-2">
|
|
<h2 className="text-3xl font-bold">Create your account</h2>
|
|
<p className="text-muted-foreground">
|
|
Fill in your details to get started
|
|
</p>
|
|
</div>
|
|
|
|
<Card className="border-0 shadow-lg">
|
|
<CardContent className="pt-6">
|
|
<form onSubmit={handleSubmit(handleRegister)} className="space-y-4">
|
|
{error && (
|
|
<motion.div
|
|
initial={{ opacity: 0, scale: 0.95 }}
|
|
animate={{ opacity: 1, scale: 1 }}
|
|
>
|
|
<Alert variant="destructive">
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription>{error}</AlertDescription>
|
|
</Alert>
|
|
</motion.div>
|
|
)}
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="name">Full Name</Label>
|
|
<div className="relative">
|
|
<UserIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
id="name"
|
|
type="text"
|
|
placeholder="Your full name"
|
|
className="pl-10"
|
|
{...register("name")}
|
|
/>
|
|
</div>
|
|
{errors.name && (
|
|
<p className="text-sm text-destructive">{errors.name.message}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email">Email</Label>
|
|
<div className="relative">
|
|
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
placeholder="you@example.com"
|
|
className="pl-10"
|
|
{...register("email")}
|
|
/>
|
|
</div>
|
|
{errors.email && (
|
|
<p className="text-sm text-destructive">{errors.email.message}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="phone">
|
|
Phone <span className="text-muted-foreground font-normal">(optional)</span>
|
|
</Label>
|
|
<div className="relative">
|
|
<Phone className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
id="phone"
|
|
type="tel"
|
|
placeholder="+1 555 000 0000"
|
|
className="pl-10"
|
|
{...register("phone")}
|
|
/>
|
|
</div>
|
|
{errors.phone && (
|
|
<p className="text-sm text-destructive">{errors.phone.message}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="password">Password</Label>
|
|
<div className="relative">
|
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
id="password"
|
|
type={showPassword ? "text" : "password"}
|
|
placeholder="At least 6 characters"
|
|
className="pl-10 pr-10"
|
|
{...register("password")}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
>
|
|
{showPassword ? (
|
|
<EyeOff className="h-4 w-4 text-muted-foreground" />
|
|
) : (
|
|
<Eye className="h-4 w-4 text-muted-foreground" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
{errors.password && (
|
|
<p className="text-sm text-destructive">{errors.password.message}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
|
<div className="relative">
|
|
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
|
<Input
|
|
id="confirmPassword"
|
|
type={showConfirmPassword ? "text" : "password"}
|
|
placeholder="Re-enter your password"
|
|
className="pl-10 pr-10"
|
|
{...register("confirmPassword")}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
|
|
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
>
|
|
{showConfirmPassword ? (
|
|
<EyeOff className="h-4 w-4 text-muted-foreground" />
|
|
) : (
|
|
<Eye className="h-4 w-4 text-muted-foreground" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
{errors.confirmPassword && (
|
|
<p className="text-sm text-destructive">{errors.confirmPassword.message}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-start space-x-2">
|
|
<Checkbox id="acceptTerms" {...register("acceptTerms")} className="mt-1" />
|
|
<Label
|
|
htmlFor="acceptTerms"
|
|
className="text-sm font-normal cursor-pointer leading-relaxed"
|
|
>
|
|
I agree to the{" "}
|
|
<Link href="/terms" className="text-primary hover:underline">
|
|
Terms of Use
|
|
</Link>
|
|
{" "}and{" "}
|
|
<Link href="/privacy" className="text-primary hover:underline">
|
|
Privacy Policy
|
|
</Link>
|
|
</Label>
|
|
</div>
|
|
{errors.acceptTerms && (
|
|
<p className="text-sm text-destructive">{errors.acceptTerms.message}</p>
|
|
)}
|
|
|
|
<Button
|
|
type="submit"
|
|
className="w-full h-11 cursor-pointer bg-[#F0932B] hover:bg-[#d97d1a]"
|
|
disabled={loading}
|
|
>
|
|
{loading ? "Creating account..." : "Create Account"}
|
|
</Button>
|
|
</form>
|
|
|
|
<div className="mt-6 text-center">
|
|
<p className="text-sm text-muted-foreground">
|
|
Already have an account?{" "}
|
|
<Link href="/login" className="text-primary hover:underline font-semibold">
|
|
Log in
|
|
</Link>
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className="text-center">
|
|
<Link
|
|
href="/"
|
|
className="text-sm text-muted-foreground hover:text-foreground transition-colors inline-flex items-center gap-2"
|
|
>
|
|
Back to home
|
|
</Link>
|
|
</div>
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
|
|
<Footer />
|
|
</div>
|
|
);
|
|
}
|