fix: remove middleware and route groups to fix Vercel deployment
- Remove middleware.ts entirely (caused __dirname ReferenceError in Edge) - Auth protection handled by dashboard layout.tsx (server-side redirect) - Move pages out of (marketing) and (auth) route groups to fix 404 on / - Keep (protected) route group for dashboard/formations/profil shared layout https://claude.ai/code/session_01H2aRGDaKgarPvhay2HxN6Y
This commit is contained in:
427
app/candidature/page.tsx
Normal file
427
app/candidature/page.tsx
Normal file
@@ -0,0 +1,427 @@
|
||||
"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'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user