Files
obc-terrassement/app/(marketing)/candidature/page.tsx
Claude 41e686c560 feat: complete HookLab MVP - TikTok Shop coaching platform
Full-stack Next.js 15 application with:
- Landing page with marketing components (Hero, Testimonials, Pricing, FAQ)
- Multi-step candidature form with API route
- Stripe Checkout integration (subscription + webhooks)
- Supabase Auth (login/register) with middleware protection
- Dashboard with progress tracking and module system
- Formations pages with completion tracking
- Profile management with password change
- Database schema with RLS policies
- Resend email integration for transactional emails

Stack: Next.js 15, TypeScript, Tailwind CSS v4, Supabase, Stripe, Resend

https://claude.ai/code/session_01H2aRGDaKgarPvhay2HxN6Y
2026-02-08 12:39:18 +00:00

428 lines
14 KiB
TypeScript

"use client";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import Button from "@/components/ui/Button";
import Input, { Textarea } from "@/components/ui/Input";
// Étapes du formulaire
type Step = 1 | 2 | 3;
interface FormData {
firstname: string;
email: string;
phone: string;
persona: string;
age: string;
experience: string;
time_daily: string;
availability: string;
start_date: string;
motivation: string;
monthly_goal: string;
biggest_fear: string;
tiktok_username: string;
}
const initialFormData: FormData = {
firstname: "",
email: "",
phone: "",
persona: "",
age: "",
experience: "",
time_daily: "",
availability: "",
start_date: "",
motivation: "",
monthly_goal: "",
biggest_fear: "",
tiktok_username: "",
};
export default function CandidaturePage() {
const router = useRouter();
const [step, setStep] = useState<Step>(1);
const [formData, setFormData] = useState<FormData>(initialFormData);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const updateField = (field: keyof FormData, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const canGoNext = (): boolean => {
switch (step) {
case 1:
return !!(
formData.firstname &&
formData.email &&
formData.phone &&
formData.persona &&
formData.age
);
case 2:
return !!(
formData.experience &&
formData.time_daily &&
formData.availability &&
formData.start_date
);
case 3:
return !!(
formData.motivation &&
formData.monthly_goal &&
formData.biggest_fear
);
default:
return false;
}
};
const handleSubmit = async () => {
setLoading(true);
setError(null);
try {
const res = await fetch("/api/candidature", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...formData,
age: parseInt(formData.age, 10),
}),
});
if (!res.ok) {
const data = await res.json();
throw new Error(data.error || "Erreur lors de l'envoi");
}
router.push("/merci");
} catch (err) {
setError(err instanceof Error ? err.message : "Erreur inattendue");
} finally {
setLoading(false);
}
};
return (
<main className="min-h-screen py-20 md:py-32">
<div className="max-w-2xl mx-auto px-4 sm:px-6">
{/* Header */}
<div className="text-center mb-12">
<Link href="/" className="inline-flex items-center gap-2 mb-8">
<div className="w-8 h-8 gradient-bg rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-sm">H</span>
</div>
<span className="text-xl font-bold text-white">
Hook<span className="gradient-text">Lab</span>
</span>
</Link>
<h1 className="text-3xl md:text-4xl font-bold tracking-[-0.02em] mb-3">
Candidature <span className="gradient-text">HookLab</span>
</h1>
<p className="text-white/60">
Reponds a quelques questions pour qu&apos;on puisse evaluer ton
profil.
</p>
</div>
{/* Progress bar */}
<div className="flex items-center gap-2 mb-10">
{[1, 2, 3].map((s) => (
<div
key={s}
className={`h-1.5 flex-1 rounded-full transition-colors ${
s <= step ? "gradient-bg" : "bg-dark-lighter"
}`}
/>
))}
</div>
{/* Formulaire */}
<div className="bg-dark-light border border-dark-border rounded-[20px] p-6 md:p-8">
{/* Étape 1 : Informations personnelles */}
{step === 1 && (
<div className="space-y-5">
<h2 className="text-xl font-bold text-white mb-6">
Informations personnelles
</h2>
<Input
id="firstname"
label="Prenom"
placeholder="Ton prenom"
value={formData.firstname}
onChange={(e) => updateField("firstname", e.target.value)}
/>
<Input
id="email"
label="Email"
type="email"
placeholder="ton@email.com"
value={formData.email}
onChange={(e) => updateField("email", e.target.value)}
/>
<Input
id="phone"
label="Telephone"
type="tel"
placeholder="06 12 34 56 78"
value={formData.phone}
onChange={(e) => updateField("phone", e.target.value)}
/>
<Input
id="age"
label="Age"
type="number"
placeholder="25"
min="18"
max="65"
value={formData.age}
onChange={(e) => updateField("age", e.target.value)}
/>
{/* Persona selection */}
<div className="space-y-1.5">
<label className="block text-sm font-medium text-white/80">
Tu es plutot...
</label>
<div className="grid grid-cols-2 gap-3">
{[
{
id: "jeune",
label: "Etudiant / Jeune",
emoji: "🎓",
},
{
id: "parent",
label: "Parent / Reconversion",
emoji: "👨‍👩‍👧",
},
].map((p) => (
<button
key={p.id}
type="button"
className={`p-4 rounded-2xl border-2 text-left transition-all cursor-pointer ${
formData.persona === p.id
? "border-primary bg-primary/10"
: "border-dark-border bg-dark-lighter hover:border-primary/30"
}`}
onClick={() => updateField("persona", p.id)}
>
<span className="text-2xl block mb-2">{p.emoji}</span>
<span className="text-white text-sm font-medium">
{p.label}
</span>
</button>
))}
</div>
</div>
</div>
)}
{/* Étape 2 : Situation actuelle */}
{step === 2 && (
<div className="space-y-5">
<h2 className="text-xl font-bold text-white mb-6">
Ta situation actuelle
</h2>
<div className="space-y-1.5">
<label className="block text-sm font-medium text-white/80">
Experience e-commerce / reseaux sociaux
</label>
<div className="space-y-2">
{[
"Debutant complet",
"J'ai deja teste des choses",
"Je genere deja des revenus en ligne",
].map((opt) => (
<button
key={opt}
type="button"
className={`w-full p-3 rounded-xl border text-left text-sm transition-all cursor-pointer ${
formData.experience === opt
? "border-primary bg-primary/10 text-white"
: "border-dark-border bg-dark-lighter text-white/60 hover:border-primary/30"
}`}
onClick={() => updateField("experience", opt)}
>
{opt}
</button>
))}
</div>
</div>
<div className="space-y-1.5">
<label className="block text-sm font-medium text-white/80">
Temps disponible par jour
</label>
<div className="space-y-2">
{["1-2 heures", "2-4 heures", "4+ heures", "Temps plein"].map(
(opt) => (
<button
key={opt}
type="button"
className={`w-full p-3 rounded-xl border text-left text-sm transition-all cursor-pointer ${
formData.time_daily === opt
? "border-primary bg-primary/10 text-white"
: "border-dark-border bg-dark-lighter text-white/60 hover:border-primary/30"
}`}
onClick={() => updateField("time_daily", opt)}
>
{opt}
</button>
)
)}
</div>
</div>
<div className="space-y-1.5">
<label className="block text-sm font-medium text-white/80">
Disponibilite pour commencer
</label>
<div className="space-y-2">
{[
"Immediatement",
"Dans 1-2 semaines",
"Dans 1 mois",
].map((opt) => (
<button
key={opt}
type="button"
className={`w-full p-3 rounded-xl border text-left text-sm transition-all cursor-pointer ${
formData.availability === opt
? "border-primary bg-primary/10 text-white"
: "border-dark-border bg-dark-lighter text-white/60 hover:border-primary/30"
}`}
onClick={() => updateField("availability", opt)}
>
{opt}
</button>
))}
</div>
</div>
<div className="space-y-1.5">
<label className="block text-sm font-medium text-white/80">
Quand souhaites-tu commencer ?
</label>
<div className="space-y-2">
{[
"Cette semaine",
"La semaine prochaine",
"Ce mois-ci",
].map((opt) => (
<button
key={opt}
type="button"
className={`w-full p-3 rounded-xl border text-left text-sm transition-all cursor-pointer ${
formData.start_date === opt
? "border-primary bg-primary/10 text-white"
: "border-dark-border bg-dark-lighter text-white/60 hover:border-primary/30"
}`}
onClick={() => updateField("start_date", opt)}
>
{opt}
</button>
))}
</div>
</div>
</div>
)}
{/* Étape 3 : Motivation */}
{step === 3 && (
<div className="space-y-5">
<h2 className="text-xl font-bold text-white mb-6">
Ta motivation
</h2>
<Textarea
id="motivation"
label="Pourquoi veux-tu rejoindre HookLab ?"
placeholder="Parle-nous de tes objectifs, de ce qui te motive..."
rows={4}
value={formData.motivation}
onChange={(e) => updateField("motivation", e.target.value)}
/>
<Input
id="monthly_goal"
label="Objectif de revenus mensuels"
placeholder="Ex: 1000€/mois"
value={formData.monthly_goal}
onChange={(e) => updateField("monthly_goal", e.target.value)}
/>
<Textarea
id="biggest_fear"
label="Quelle est ta plus grande peur ?"
placeholder="Qu'est-ce qui pourrait t'empecher de reussir ?"
rows={3}
value={formData.biggest_fear}
onChange={(e) => updateField("biggest_fear", e.target.value)}
/>
<Input
id="tiktok_username"
label="Pseudo TikTok (optionnel)"
placeholder="@tonpseudo"
value={formData.tiktok_username}
onChange={(e) => updateField("tiktok_username", e.target.value)}
/>
</div>
)}
{/* Error */}
{error && (
<div className="mt-4 p-3 bg-error/10 border border-error/20 rounded-xl">
<p className="text-error text-sm">{error}</p>
</div>
)}
{/* Navigation */}
<div className="flex items-center justify-between mt-8 pt-6 border-t border-dark-border">
{step > 1 ? (
<Button
variant="ghost"
onClick={() => setStep((step - 1) as Step)}
>
Retour
</Button>
) : (
<Link href="/">
<Button variant="ghost">Annuler</Button>
</Link>
)}
{step < 3 ? (
<Button
onClick={() => setStep((step + 1) as Step)}
disabled={!canGoNext()}
>
Continuer
</Button>
) : (
<Button
onClick={handleSubmit}
loading={loading}
disabled={!canGoNext()}
>
Envoyer ma candidature
</Button>
)}
</div>
</div>
{/* Step indicator text */}
<p className="text-center text-white/30 text-sm mt-4">
Etape {step} sur 3
</p>
</div>
</main>
);
}