- 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>
620 lines
25 KiB
TypeScript
620 lines
25 KiB
TypeScript
"use client";
|
||
|
||
import { useState } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import Link from "next/link";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Checkbox } from "@/components/ui/checkbox";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/components/ui/select";
|
||
import { Textarea } from "@/components/ui/textarea";
|
||
import {
|
||
Building2,
|
||
Mail,
|
||
Lock,
|
||
Eye,
|
||
EyeOff,
|
||
Phone,
|
||
Globe,
|
||
MapPin,
|
||
ArrowLeft,
|
||
CheckCircle2,
|
||
Briefcase,
|
||
Loader2,
|
||
} from "lucide-react";
|
||
import { motion } from "framer-motion";
|
||
import { useForm } from "react-hook-form";
|
||
import { zodResolver } from "@hookform/resolvers/zod";
|
||
import { z } from "zod";
|
||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||
import { paymentsApi } from "@/lib/api";
|
||
import { Navbar } from "@/components/navbar";
|
||
import { Footer } from "@/components/footer";
|
||
|
||
const COUNTRIES = [
|
||
"Argentina", "Australia", "Austria", "Belgium", "Brazil", "Canada", "Chile",
|
||
"China", "Colombia", "Czech Republic", "Denmark", "Egypt", "Finland", "France",
|
||
"Germany", "Greece", "Hong Kong", "Hungary", "India", "Indonesia", "Ireland",
|
||
"Israel", "Italy", "Japan", "Malaysia", "Mexico", "Netherlands", "New Zealand",
|
||
"Nigeria", "Norway", "Pakistan", "Peru", "Philippines", "Poland", "Portugal",
|
||
"Romania", "Saudi Arabia", "Singapore", "South Africa", "South Korea", "Spain",
|
||
"Sweden", "Switzerland", "Taiwan", "Thailand", "Turkey", "Ukraine",
|
||
"United Arab Emirates", "United Kingdom", "United States", "Vietnam",
|
||
];
|
||
|
||
const YEARS_IN_MARKET = [
|
||
{ value: "<1", label: "Less than 1 year" },
|
||
{ value: "1-3", label: "1 – 3 years" },
|
||
{ value: "3-5", label: "3 – 5 years" },
|
||
{ value: "5-10", label: "5 – 10 years" },
|
||
{ value: "10+", label: "10+ years" },
|
||
];
|
||
|
||
interface Plan {
|
||
id: string;
|
||
name: string;
|
||
price: string;
|
||
period: string;
|
||
priceId: string | null; // Stripe Price ID — null for free/enterprise
|
||
features: string[];
|
||
recommended: boolean;
|
||
cta: string;
|
||
}
|
||
|
||
const PLANS: Plan[] = [
|
||
{
|
||
id: "free",
|
||
name: "Starter",
|
||
price: "$0",
|
||
period: "/ month",
|
||
priceId: null,
|
||
features: [
|
||
"1 active job posting",
|
||
"Resume collection",
|
||
"Basic applicant dashboard",
|
||
"Email notifications",
|
||
],
|
||
recommended: false,
|
||
cta: "Get started free",
|
||
},
|
||
{
|
||
id: "pro",
|
||
name: "Pro",
|
||
price: "$49",
|
||
period: "/ month",
|
||
priceId: process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID || null,
|
||
features: [
|
||
"Unlimited job postings",
|
||
"Priority in search results",
|
||
"Talent pool access",
|
||
"Priority support",
|
||
"Analytics dashboard",
|
||
],
|
||
recommended: true,
|
||
cta: "Start Pro",
|
||
},
|
||
{
|
||
id: "enterprise",
|
||
name: "Enterprise",
|
||
price: "Custom",
|
||
period: "",
|
||
priceId: null,
|
||
features: [
|
||
"Everything in Pro",
|
||
"API integration",
|
||
"Dedicated account manager",
|
||
"Advanced reporting",
|
||
"White-label option",
|
||
],
|
||
recommended: false,
|
||
cta: "Contact sales",
|
||
},
|
||
];
|
||
|
||
const companySchema = z
|
||
.object({
|
||
companyName: z.string().min(2, "Company name is required"),
|
||
email: z.string().email("Invalid email address"),
|
||
password: z.string().min(8, "Password must be at least 8 characters"),
|
||
confirmPassword: z.string(),
|
||
phone: z.string().optional(),
|
||
country: z.string().min(1, "Country is required"),
|
||
city: z.string().optional(),
|
||
website: z.string().url("Invalid URL").optional().or(z.literal("")),
|
||
yearsInMarket: z.string().min(1, "Years in market is required"),
|
||
description: z.string().min(20, "Description must be at least 20 characters"),
|
||
taxId: z.string().optional(),
|
||
acceptTerms: z.boolean().refine((v) => v === true, "You must accept the terms"),
|
||
})
|
||
.refine((d) => d.password === d.confirmPassword, {
|
||
message: "Passwords do not match",
|
||
path: ["confirmPassword"],
|
||
});
|
||
|
||
type CompanyFormData = z.infer<typeof companySchema>;
|
||
|
||
const STEPS = ["Plan", "Details", "Terms", "Confirm"];
|
||
|
||
export default function RegisterPage() {
|
||
const router = useRouter();
|
||
const [step, setStep] = useState(1);
|
||
const [selectedPlan, setSelectedPlan] = useState<Plan | null>(null);
|
||
const [showPw, setShowPw] = useState(false);
|
||
const [showConfirmPw, setShowConfirmPw] = useState(false);
|
||
const [loading, setLoading] = useState(false);
|
||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||
|
||
const {
|
||
register,
|
||
handleSubmit,
|
||
formState: { errors },
|
||
setValue,
|
||
trigger,
|
||
watch,
|
||
} = useForm<CompanyFormData>({
|
||
resolver: zodResolver(companySchema),
|
||
defaultValues: { yearsInMarket: "", country: "" },
|
||
});
|
||
|
||
const watchedCountry = watch("country");
|
||
|
||
const nextStep = async () => {
|
||
setErrorMsg(null);
|
||
if (step === 1) {
|
||
if (!selectedPlan) {
|
||
setErrorMsg("Please select a plan to continue.");
|
||
return;
|
||
}
|
||
setStep(2);
|
||
} else if (step === 2) {
|
||
const ok = await trigger([
|
||
"companyName", "email", "password", "confirmPassword",
|
||
"country", "yearsInMarket", "description",
|
||
]);
|
||
if (ok) setStep(3);
|
||
} else if (step === 3) {
|
||
const ok = await trigger("acceptTerms");
|
||
if (ok) setStep(4);
|
||
}
|
||
};
|
||
|
||
const onSubmit = async (data: CompanyFormData) => {
|
||
if (!selectedPlan) return;
|
||
|
||
setLoading(true);
|
||
setErrorMsg(null);
|
||
|
||
try {
|
||
const { registerCompany } = await import("@/lib/auth");
|
||
|
||
const res = await registerCompany({
|
||
companyName: data.companyName,
|
||
email: data.email,
|
||
phone: data.phone,
|
||
password: data.password,
|
||
document: data.taxId,
|
||
website: data.website,
|
||
yearsInMarket: data.yearsInMarket,
|
||
description: data.description,
|
||
city: data.city,
|
||
country: data.country,
|
||
});
|
||
|
||
// Auto-login if token returned
|
||
if (res?.token) {
|
||
localStorage.setItem("auth_token", res.token);
|
||
localStorage.setItem("token", res.token);
|
||
localStorage.setItem("user", JSON.stringify({
|
||
name: data.companyName,
|
||
email: data.email,
|
||
role: "recruiter",
|
||
}));
|
||
}
|
||
|
||
// Enterprise → redirect to contact / dashboard
|
||
if (selectedPlan.id === "enterprise") {
|
||
router.push("/dashboard?plan=enterprise");
|
||
return;
|
||
}
|
||
|
||
// Free plan → go straight to dashboard
|
||
if (!selectedPlan.priceId) {
|
||
router.push("/dashboard");
|
||
return;
|
||
}
|
||
|
||
// Paid plan → create Stripe checkout session
|
||
try {
|
||
const origin = window.location.origin;
|
||
const checkout = await paymentsApi.createSubscriptionCheckout({
|
||
priceId: selectedPlan.priceId,
|
||
successUrl: `${origin}/dashboard?payment=success`,
|
||
cancelUrl: `${origin}/register?payment=cancelled`,
|
||
});
|
||
|
||
if (checkout.checkoutUrl) {
|
||
window.location.href = checkout.checkoutUrl;
|
||
return;
|
||
}
|
||
} catch (stripeError) {
|
||
console.warn("Stripe checkout failed, falling back:", stripeError);
|
||
// Fallback: redirect to dashboard with a pending payment notice
|
||
router.push("/dashboard?payment=pending");
|
||
return;
|
||
}
|
||
|
||
router.push("/dashboard");
|
||
} catch (error: unknown) {
|
||
const msg = error instanceof Error ? error.message : "Registration failed. Please try again.";
|
||
setErrorMsg(msg);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-background flex flex-col">
|
||
<Navbar />
|
||
|
||
<main className="flex-1 container mx-auto py-10 px-4 max-w-5xl">
|
||
{/* Step indicator */}
|
||
<div className="mb-10 max-w-3xl mx-auto">
|
||
<div className="flex items-center justify-between relative">
|
||
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-full h-0.5 bg-muted -z-10" />
|
||
{STEPS.map((label, i) => (
|
||
<div key={label} className="flex flex-col items-center gap-1">
|
||
<div
|
||
className={`w-9 h-9 rounded-full flex items-center justify-center font-bold text-sm transition-colors z-10 ${
|
||
step > i + 1
|
||
? "bg-green-500 text-white"
|
||
: step === i + 1
|
||
? "bg-primary text-primary-foreground"
|
||
: "bg-muted text-muted-foreground"
|
||
}`}
|
||
>
|
||
{step > i + 1 ? <CheckCircle2 className="w-4 h-4" /> : i + 1}
|
||
</div>
|
||
<span className="text-xs text-muted-foreground hidden sm:block">{label}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{errorMsg && (
|
||
<Alert variant="destructive" className="mb-6 max-w-2xl mx-auto">
|
||
<AlertDescription>{errorMsg}</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
<form onSubmit={handleSubmit(onSubmit)}>
|
||
{/* STEP 1: PLAN SELECTION */}
|
||
{step === 1 && (
|
||
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="space-y-8">
|
||
<div className="text-center space-y-2">
|
||
<h1 className="text-3xl font-bold">Choose your plan</h1>
|
||
<p className="text-muted-foreground">Start hiring the best talent worldwide today.</p>
|
||
</div>
|
||
|
||
<div className="grid md:grid-cols-3 gap-6">
|
||
{PLANS.map((plan) => (
|
||
<Card
|
||
key={plan.id}
|
||
className={`relative cursor-pointer transition-all hover:shadow-lg ${
|
||
selectedPlan?.id === plan.id ? "border-primary ring-2 ring-primary/20" : ""
|
||
}`}
|
||
onClick={() => { setSelectedPlan(plan); setErrorMsg(null); }}
|
||
>
|
||
{plan.recommended && (
|
||
<div className="absolute -top-3 left-1/2 -translate-x-1/2 bg-primary text-primary-foreground text-xs px-3 py-1 rounded-full whitespace-nowrap">
|
||
Recommended
|
||
</div>
|
||
)}
|
||
<CardHeader className="pb-2">
|
||
<CardTitle className="text-lg">{plan.name}</CardTitle>
|
||
<div className="mt-1">
|
||
<span className="text-4xl font-bold">{plan.price}</span>
|
||
{plan.period && (
|
||
<span className="text-muted-foreground text-sm ml-1">{plan.period}</span>
|
||
)}
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<ul className="space-y-2 text-sm">
|
||
{plan.features.map((f) => (
|
||
<li key={f} className="flex items-center gap-2">
|
||
<CheckCircle2 className="w-4 h-4 text-green-500 shrink-0" />
|
||
{f}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</CardContent>
|
||
<CardFooter>
|
||
<div
|
||
className={`w-full h-4 rounded-full border-2 flex items-center justify-center transition-colors ${
|
||
selectedPlan?.id === plan.id ? "border-primary" : "border-muted"
|
||
}`}
|
||
>
|
||
{selectedPlan?.id === plan.id && (
|
||
<div className="w-2.5 h-2.5 bg-primary rounded-full" />
|
||
)}
|
||
</div>
|
||
</CardFooter>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
|
||
{/* STEP 2: COMPANY DETAILS */}
|
||
{step === 2 && (
|
||
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="max-w-2xl mx-auto space-y-5">
|
||
<div className="text-center space-y-1 mb-4">
|
||
<h2 className="text-2xl font-bold">Company details</h2>
|
||
<p className="text-muted-foreground">Fill in your company and account information.</p>
|
||
</div>
|
||
|
||
{/* Company name + Years in market */}
|
||
<div className="grid md:grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>Company name *</Label>
|
||
<div className="relative">
|
||
<Building2 className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||
<Input {...register("companyName")} className="pl-10" placeholder="Acme Inc." />
|
||
</div>
|
||
{errors.companyName && <p className="text-xs text-destructive">{errors.companyName.message}</p>}
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label>Years in business *</Label>
|
||
<Select onValueChange={(v) => { setValue("yearsInMarket", v); trigger("yearsInMarket"); }}>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="Select…" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{YEARS_IN_MARKET.map((y) => (
|
||
<SelectItem key={y.value} value={y.value}>{y.label}</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
{errors.yearsInMarket && <p className="text-xs text-destructive">{errors.yearsInMarket.message}</p>}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Description */}
|
||
<div className="space-y-2">
|
||
<Label>Company description *</Label>
|
||
<Textarea
|
||
{...register("description")}
|
||
placeholder="Brief description of what your company does, its mission and values…"
|
||
className="min-h-[90px]"
|
||
/>
|
||
{errors.description && <p className="text-xs text-destructive">{errors.description.message}</p>}
|
||
</div>
|
||
|
||
{/* Email + Phone */}
|
||
<div className="grid md:grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>Email *</Label>
|
||
<div className="relative">
|
||
<Mail className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||
<Input type="email" {...register("email")} className="pl-10" placeholder="hr@acme.com" />
|
||
</div>
|
||
{errors.email && <p className="text-xs text-destructive">{errors.email.message}</p>}
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label>Phone <span className="text-muted-foreground text-xs">(optional)</span></Label>
|
||
<div className="relative">
|
||
<Phone className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||
<Input {...register("phone")} className="pl-10" placeholder="+1 555 000 0000" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Password + Confirm */}
|
||
<div className="grid md:grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>Password *</Label>
|
||
<div className="relative">
|
||
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||
<Input
|
||
type={showPw ? "text" : "password"}
|
||
{...register("password")}
|
||
className="pl-10 pr-10"
|
||
placeholder="Min. 8 characters"
|
||
/>
|
||
<button type="button" onClick={() => setShowPw(!showPw)} className="absolute right-3 top-3 text-muted-foreground">
|
||
{showPw ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
{errors.password && <p className="text-xs text-destructive">{errors.password.message}</p>}
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label>Confirm password *</Label>
|
||
<div className="relative">
|
||
<Lock className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||
<Input
|
||
type={showConfirmPw ? "text" : "password"}
|
||
{...register("confirmPassword")}
|
||
className="pl-10 pr-10"
|
||
placeholder="Repeat password"
|
||
/>
|
||
<button type="button" onClick={() => setShowConfirmPw(!showConfirmPw)} className="absolute right-3 top-3 text-muted-foreground">
|
||
{showConfirmPw ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
||
</button>
|
||
</div>
|
||
{errors.confirmPassword && <p className="text-xs text-destructive">{errors.confirmPassword.message}</p>}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Country + City */}
|
||
<div className="grid md:grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>Country *</Label>
|
||
<Select
|
||
value={watchedCountry}
|
||
onValueChange={(v) => { setValue("country", v); trigger("country"); }}
|
||
>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="Select country…" />
|
||
</SelectTrigger>
|
||
<SelectContent className="max-h-60">
|
||
{COUNTRIES.map((c) => (
|
||
<SelectItem key={c} value={c}>{c}</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
{errors.country && <p className="text-xs text-destructive">{errors.country.message}</p>}
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label>City <span className="text-muted-foreground text-xs">(optional)</span></Label>
|
||
<div className="relative">
|
||
<MapPin className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||
<Input {...register("city")} className="pl-10" placeholder="San Francisco" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Website + Tax ID */}
|
||
<div className="grid md:grid-cols-2 gap-4">
|
||
<div className="space-y-2">
|
||
<Label>Website <span className="text-muted-foreground text-xs">(optional)</span></Label>
|
||
<div className="relative">
|
||
<Globe className="absolute left-3 top-3 h-4 w-4 text-muted-foreground" />
|
||
<Input {...register("website")} className="pl-10" placeholder="https://acme.com" />
|
||
</div>
|
||
{errors.website && <p className="text-xs text-destructive">{errors.website.message}</p>}
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label>Tax ID / Business number <span className="text-muted-foreground text-xs">(optional)</span></Label>
|
||
<Input {...register("taxId")} placeholder="EIN, CNPJ, ABN, CRN, etc." />
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
|
||
{/* STEP 3: TERMS */}
|
||
{step === 3 && (
|
||
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="max-w-2xl mx-auto space-y-6">
|
||
<div className="text-center space-y-1 mb-4">
|
||
<h2 className="text-2xl font-bold">Terms & Conditions</h2>
|
||
<p className="text-muted-foreground">Please read carefully before proceeding.</p>
|
||
</div>
|
||
|
||
<div className="h-64 overflow-y-auto border rounded-md p-4 bg-muted/20 text-sm leading-relaxed space-y-3">
|
||
<h4 className="font-bold">1. Acceptance</h4>
|
||
<p>By creating an account on GoHorse Jobs, you agree to these terms and our Privacy Policy. If you do not agree, please do not use the platform.</p>
|
||
<h4 className="font-bold">2. Platform Use</h4>
|
||
<p>The platform is designed to connect companies with candidates globally. You agree to post only accurate, lawful, and non-discriminatory job listings.</p>
|
||
<h4 className="font-bold">3. Payments & Subscriptions</h4>
|
||
<p>Paid plans are billed on a recurring basis. You may cancel at any time. Refunds are subject to our refund policy. All prices are in USD unless stated otherwise.</p>
|
||
<h4 className="font-bold">4. Data & Privacy</h4>
|
||
<p>We process personal data in accordance with applicable laws (GDPR, LGPD, etc.). Candidate data is shared only with the posting employer. You are responsible for handling applicant data lawfully.</p>
|
||
<h4 className="font-bold">5. Prohibited Conduct</h4>
|
||
<p>You must not post fraudulent, misleading, or discriminatory job listings. Violations may result in immediate account suspension.</p>
|
||
<h4 className="font-bold">6. Liability</h4>
|
||
<p>GoHorse Jobs is not liable for any employment decisions made based on platform use. The platform is provided "as is" with no warranties of any kind.</p>
|
||
</div>
|
||
|
||
<div className="flex items-start space-x-2 pt-2">
|
||
<Checkbox id="terms" onCheckedChange={(v) => setValue("acceptTerms", v as boolean)} />
|
||
<label htmlFor="terms" className="text-sm leading-relaxed cursor-pointer">
|
||
I have read and agree to the{" "}
|
||
<Link href="/terms" className="text-primary underline">Terms of Use</Link>
|
||
{" "}and{" "}
|
||
<Link href="/privacy" className="text-primary underline">Privacy Policy</Link>
|
||
{" "}of GoHorse Jobs.
|
||
</label>
|
||
</div>
|
||
{errors.acceptTerms && <p className="text-xs text-destructive">{errors.acceptTerms.message}</p>}
|
||
</motion.div>
|
||
)}
|
||
|
||
{/* STEP 4: CONFIRM & SUBMIT */}
|
||
{step === 4 && (
|
||
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} className="max-w-md mx-auto space-y-8 text-center">
|
||
<div className="w-20 h-20 bg-primary/10 rounded-full flex items-center justify-center mx-auto text-primary">
|
||
{selectedPlan?.id === "free" ? (
|
||
<CheckCircle2 className="w-10 h-10" />
|
||
) : selectedPlan?.id === "enterprise" ? (
|
||
<Briefcase className="w-10 h-10" />
|
||
) : (
|
||
<Globe className="w-10 h-10" />
|
||
)}
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<h2 className="text-2xl font-bold">Almost there!</h2>
|
||
<p className="text-muted-foreground">
|
||
You selected the <strong>{selectedPlan?.name}</strong> plan.
|
||
</p>
|
||
<p className="text-sm text-muted-foreground">
|
||
{selectedPlan?.id === "free"
|
||
? "Your account will be created and you'll be redirected to the dashboard."
|
||
: selectedPlan?.id === "enterprise"
|
||
? "Your account will be created and our team will reach out to discuss your needs."
|
||
: "Your account will be created, then you'll be redirected to Stripe to complete payment."}
|
||
</p>
|
||
</div>
|
||
|
||
<div className="bg-muted p-4 rounded-lg text-left space-y-2">
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-muted-foreground">Plan</span>
|
||
<span className="font-medium">{selectedPlan?.name}</span>
|
||
</div>
|
||
<div className="flex justify-between text-sm">
|
||
<span className="text-muted-foreground">Billing</span>
|
||
<span className="font-bold">
|
||
{selectedPlan?.price}{selectedPlan?.period ? ` ${selectedPlan.period}` : ""}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</motion.div>
|
||
)}
|
||
|
||
{/* Navigation */}
|
||
<div className="mt-8 flex justify-between max-w-2xl mx-auto px-0">
|
||
{step > 1 ? (
|
||
<Button type="button" variant="outline" onClick={() => setStep(step - 1)} disabled={loading}>
|
||
<ArrowLeft className="w-4 h-4 mr-2" /> Back
|
||
</Button>
|
||
) : (
|
||
<div />
|
||
)}
|
||
|
||
{step < 4 ? (
|
||
<Button type="button" onClick={nextStep} className="min-w-[120px]">
|
||
Next
|
||
</Button>
|
||
) : (
|
||
<Button type="submit" disabled={loading} className="min-w-[160px]">
|
||
{loading ? (
|
||
<><Loader2 className="w-4 h-4 mr-2 animate-spin" /> Processing…</>
|
||
) : selectedPlan?.id === "free" ? (
|
||
"Create account"
|
||
) : selectedPlan?.id === "enterprise" ? (
|
||
"Create account"
|
||
) : (
|
||
"Create account & pay"
|
||
)}
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</form>
|
||
</main>
|
||
|
||
<Footer />
|
||
</div>
|
||
);
|
||
}
|