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
This commit is contained in:
Claude
2026-02-08 12:39:18 +00:00
parent 240b10b2d7
commit 41e686c560
52 changed files with 11375 additions and 4 deletions

View File

@@ -0,0 +1,146 @@
import Link from "next/link";
import Card from "@/components/ui/Card";
import type { Module, UserProgress } from "@/types/database.types";
interface ModuleCardProps {
module: Module;
progress?: UserProgress;
}
const contentTypeIcons: Record<string, React.ReactNode> = {
video: (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
),
pdf: (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
),
text: (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M4 6h16M4 12h16M4 18h7"
/>
</svg>
),
quiz: (
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
),
};
export default function ModuleCard({ module, progress }: ModuleCardProps) {
const isCompleted = progress?.completed;
return (
<Link href={`/formations/${module.id}`}>
<Card hover className="relative overflow-hidden group">
{/* Status indicator */}
{isCompleted && (
<div className="absolute top-4 right-4">
<div className="w-6 h-6 rounded-full bg-success flex items-center justify-center">
<svg
className="w-3.5 h-3.5 text-white"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
</div>
)}
{/* Content type + Duration */}
<div className="flex items-center gap-3 mb-3">
<span
className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium ${
isCompleted
? "bg-success/10 text-success"
: "bg-primary/10 text-primary"
}`}
>
{module.content_type && contentTypeIcons[module.content_type]}
{module.content_type?.toUpperCase() || "CONTENU"}
</span>
{module.duration_minutes && (
<span className="text-white/30 text-xs">
{module.duration_minutes} min
</span>
)}
</div>
{/* Title */}
<h3 className="text-white font-semibold mb-2 group-hover:text-primary transition-colors">
{module.title}
</h3>
{/* Description */}
{module.description && (
<p className="text-white/50 text-sm line-clamp-2">
{module.description}
</p>
)}
{/* Week badge */}
<div className="mt-4 pt-3 border-t border-dark-border">
<span className="text-white/30 text-xs">
Semaine {module.week_number}
</span>
</div>
</Card>
</Link>
);
}

View File

@@ -0,0 +1,36 @@
interface ProgressBarProps {
value: number; // 0-100
label?: string;
showPercentage?: boolean;
}
export default function ProgressBar({
value,
label,
showPercentage = true,
}: ProgressBarProps) {
const clampedValue = Math.min(100, Math.max(0, value));
return (
<div className="space-y-2">
{(label || showPercentage) && (
<div className="flex items-center justify-between">
{label && (
<span className="text-white/60 text-sm">{label}</span>
)}
{showPercentage && (
<span className="text-white font-medium text-sm">
{Math.round(clampedValue)}%
</span>
)}
</div>
)}
<div className="h-2 bg-dark-lighter rounded-full overflow-hidden">
<div
className="h-full gradient-bg rounded-full transition-all duration-500 ease-out"
style={{ width: `${clampedValue}%` }}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,156 @@
"use client";
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { createClient } from "@/lib/supabase/client";
import { cn } from "@/lib/utils";
import type { Profile } from "@/types/database.types";
interface SidebarProps {
user: Profile;
}
const navItems = [
{
label: "Dashboard",
href: "/dashboard",
icon: (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
),
},
{
label: "Formations",
href: "/formations",
icon: (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
),
},
{
label: "Profil",
href: "/profil",
icon: (
<svg
className="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"
/>
</svg>
),
},
];
export default function Sidebar({ user }: SidebarProps) {
const pathname = usePathname();
const router = useRouter();
const handleLogout = async () => {
const supabase = createClient();
await supabase.auth.signOut();
router.push("/login");
router.refresh();
};
return (
<aside className="w-64 min-h-screen bg-dark-light border-r border-dark-border p-6 flex flex-col">
{/* Logo */}
<Link href="/dashboard" className="flex items-center gap-2 mb-10">
<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>
{/* Navigation */}
<nav className="flex-1 space-y-1">
{navItems.map((item) => {
const isActive =
pathname === item.href ||
(item.href !== "/dashboard" && pathname.startsWith(item.href));
return (
<Link
key={item.href}
href={item.href}
className={cn(
"flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all",
isActive
? "bg-primary/10 text-primary"
: "text-white/50 hover:text-white hover:bg-white/5"
)}
>
{item.icon}
{item.label}
</Link>
);
})}
</nav>
{/* User info + Logout */}
<div className="border-t border-dark-border pt-4 mt-4">
<div className="flex items-center gap-3 mb-4">
<div className="w-9 h-9 gradient-bg rounded-full flex items-center justify-center text-sm font-bold text-white">
{(user.full_name || user.email)[0].toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<p className="text-white text-sm font-medium truncate">
{user.full_name || "Utilisateur"}
</p>
<p className="text-white/40 text-xs truncate">{user.email}</p>
</div>
</div>
<button
onClick={handleLogout}
className="flex items-center gap-2 text-white/40 hover:text-error text-sm transition-colors cursor-pointer w-full"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
/>
</svg>
Deconnexion
</button>
</div>
</aside>
);
}

View File

@@ -0,0 +1,110 @@
"use client";
import { useState } from "react";
const faqs = [
{
question: "Ai-je besoin d'experience sur TikTok ?",
answer:
"Non, aucune experience n'est requise. Notre programme part de zero et t'accompagne etape par etape. Beaucoup de nos eleves n'avaient jamais poste de video avant de commencer.",
},
{
question: "Combien de temps dois-je consacrer par jour ?",
answer:
"Nous recommandons un minimum de 2 heures par jour pour des resultats optimaux. Le programme est concu pour etre flexible et s'adapter a ton emploi du temps, que tu sois etudiant ou parent.",
},
{
question: "Quand vais-je voir mes premiers resultats ?",
answer:
"La plupart de nos eleves generent leurs premieres commissions dans les 2 a 4 premieres semaines. Les resultats varient selon ton implication et le temps consacre.",
},
{
question: "Dois-je investir de l'argent en plus du programme ?",
answer:
"Non. L'affiliation TikTok Shop ne necessite aucun stock ni investissement supplementaire. Tu gagnes des commissions sur les ventes generees par tes videos.",
},
{
question: "Le programme est-il adapte a tous les ages ?",
answer:
"Oui, nos eleves ont entre 18 et 55 ans. Le programme propose deux parcours adaptes : un pour les jeunes (18-25 ans) et un pour les parents/reconversion (25-45 ans).",
},
{
question: "Comment se deroule le coaching ?",
answer:
"Le coaching comprend des modules video hebdomadaires, des appels de groupe chaque semaine, un support WhatsApp illimite, et l'acces a une communaute privee d'entrepreneurs.",
},
{
question: "Puis-je payer en plusieurs fois ?",
answer:
"Oui, le paiement se fait en 2 mensualites de 490€. Le premier paiement donne acces immediat au programme, le second est preleve automatiquement le mois suivant.",
},
{
question: "Y a-t-il une garantie de remboursement ?",
answer:
"Oui, nous offrons une garantie satisfait ou rembourse de 14 jours. Si le programme ne te convient pas, tu es rembourse integralement, sans condition.",
},
];
export default function FAQ() {
const [openIndex, setOpenIndex] = useState<number | null>(null);
return (
<section id="faq" className="py-20 md:py-32 bg-dark-light/30">
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center mb-16">
<span className="inline-block px-3 py-1.5 bg-primary/10 border border-primary/20 rounded-full text-primary text-xs font-medium mb-4">
FAQ
</span>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-[-0.02em] mb-4">
Questions <span className="gradient-text">frequentes</span>
</h2>
<p className="text-white/60 text-lg">
Tout ce que tu dois savoir avant de te lancer.
</p>
</div>
{/* Accordion */}
<div className="space-y-3">
{faqs.map((faq, i) => (
<div
key={i}
className="bg-dark-light border border-dark-border rounded-2xl overflow-hidden transition-all duration-300"
>
<button
className="w-full px-6 py-5 flex items-center justify-between text-left cursor-pointer"
onClick={() => setOpenIndex(openIndex === i ? null : i)}
>
<span className="text-white font-medium pr-4">
{faq.question}
</span>
<svg
className={`w-5 h-5 text-primary shrink-0 transition-transform duration-300 ${
openIndex === i ? "rotate-180" : ""
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
{openIndex === i && (
<div className="px-6 pb-5">
<p className="text-white/60 text-sm leading-relaxed">
{faq.answer}
</p>
</div>
)}
</div>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,129 @@
import Link from "next/link";
export default function Footer() {
return (
<footer className="border-t border-dark-border py-12 md:py-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8 md:gap-12">
{/* Brand */}
<div className="md:col-span-2">
<Link href="/" className="flex items-center gap-2 mb-4">
<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>
<p className="text-white/40 text-sm max-w-xs leading-relaxed">
Le programme de coaching TikTok Shop pour lancer ton business
d&apos;affiliation en 8 semaines.
</p>
</div>
{/* Links */}
<div>
<h4 className="text-white font-semibold text-sm mb-4">
Programme
</h4>
<ul className="space-y-2.5">
<li>
<a
href="#methode"
className="text-white/40 hover:text-white text-sm transition-colors"
>
La methode
</a>
</li>
<li>
<a
href="#temoignages"
className="text-white/40 hover:text-white text-sm transition-colors"
>
Temoignages
</a>
</li>
<li>
<a
href="#tarif"
className="text-white/40 hover:text-white text-sm transition-colors"
>
Tarif
</a>
</li>
<li>
<a
href="#faq"
className="text-white/40 hover:text-white text-sm transition-colors"
>
FAQ
</a>
</li>
</ul>
</div>
<div>
<h4 className="text-white font-semibold text-sm mb-4">Legal</h4>
<ul className="space-y-2.5">
<li>
<Link
href="/mentions-legales"
className="text-white/40 hover:text-white text-sm transition-colors"
>
Mentions legales
</Link>
</li>
<li>
<Link
href="/cgv"
className="text-white/40 hover:text-white text-sm transition-colors"
>
CGV
</Link>
</li>
<li>
<Link
href="/confidentialite"
className="text-white/40 hover:text-white text-sm transition-colors"
>
Confidentialite
</Link>
</li>
</ul>
</div>
</div>
{/* Bottom */}
<div className="border-t border-dark-border mt-12 pt-8 flex flex-col md:flex-row items-center justify-between gap-4">
<p className="text-white/30 text-sm">
&copy; {new Date().getFullYear()} HookLab. Tous droits reserves.
</p>
<div className="flex items-center gap-4">
<a
href="https://tiktok.com"
target="_blank"
rel="noopener noreferrer"
className="text-white/30 hover:text-primary transition-colors"
aria-label="TikTok"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M19.59 6.69a4.83 4.83 0 01-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 01-2.88 2.5 2.89 2.89 0 01-2.89-2.89 2.89 2.89 0 012.89-2.89c.28 0 .54.04.79.11V9.01a6.27 6.27 0 00-.79-.05 6.34 6.34 0 00-6.34 6.34 6.34 6.34 0 006.34 6.34 6.34 6.34 0 006.34-6.34V8.92a8.2 8.2 0 004.76 1.52V7a4.84 4.84 0 01-1-.31z" />
</svg>
</a>
<a
href="https://instagram.com"
target="_blank"
rel="noopener noreferrer"
className="text-white/30 hover:text-primary transition-colors"
aria-label="Instagram"
>
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z" />
</svg>
</a>
</div>
</div>
</div>
</footer>
);
}

View File

@@ -0,0 +1,91 @@
import Link from "next/link";
import Button from "@/components/ui/Button";
export default function Hero() {
return (
<section className="relative pt-32 pb-20 md:pt-40 md:pb-32 overflow-hidden">
{/* Gradient background effect */}
<div className="absolute inset-0 overflow-hidden">
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 w-[800px] h-[400px] bg-primary/20 rounded-full blur-[120px]" />
</div>
<div className="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center max-w-4xl mx-auto">
{/* Badges */}
<div className="flex flex-wrap justify-center gap-3 mb-8">
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-primary/10 border border-primary/20 rounded-full text-primary text-xs font-medium">
<span className="w-1.5 h-1.5 bg-success rounded-full animate-pulse" />
Places limitees - Promo en cours
</span>
<span className="inline-flex items-center gap-1.5 px-3 py-1.5 bg-dark-light border border-dark-border rounded-full text-white/60 text-xs font-medium">
Programme 8 semaines
</span>
</div>
{/* Titre principal */}
<h1 className="text-4xl sm:text-5xl md:text-6xl lg:text-7xl font-bold tracking-[-0.02em] leading-[1.1] mb-6">
Lance ton business{" "}
<span className="gradient-text">TikTok Shop</span> en 8 semaines
</h1>
{/* Sous-titre */}
<p className="text-lg md:text-xl text-white/60 max-w-2xl mx-auto mb-10 leading-relaxed">
Le programme de coaching complet pour devenir createur affilie
TikTok Shop et generer tes premiers revenus en ligne.
</p>
{/* CTAs */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-12">
<Link href="/candidature">
<Button size="lg" className="pulse-glow text-lg px-10">
Candidater maintenant
</Button>
</Link>
<a href="#methode">
<Button variant="secondary" size="lg">
Decouvrir la methode
</Button>
</a>
</div>
{/* Social proof */}
<div className="flex flex-col sm:flex-row items-center justify-center gap-6 md:gap-10">
<div className="flex items-center gap-2">
{/* Avatars empilés */}
<div className="flex -space-x-2">
{[1, 2, 3, 4].map((i) => (
<div
key={i}
className="w-8 h-8 rounded-full gradient-bg border-2 border-dark flex items-center justify-center text-xs font-bold text-white"
>
{String.fromCharCode(64 + i)}
</div>
))}
</div>
<span className="text-sm text-white/60">
<span className="text-white font-semibold">+120</span> eleves
formes
</span>
</div>
<div className="flex items-center gap-1.5">
{[1, 2, 3, 4, 5].map((i) => (
<svg
key={i}
className="w-4 h-4 text-warning"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
))}
<span className="text-sm text-white/60 ml-1">
<span className="text-white font-semibold">4.9/5</span> de
satisfaction
</span>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,123 @@
import Card from "@/components/ui/Card";
const steps = [
{
number: "01",
title: "Apprends les bases",
description:
"Maitrise les fondamentaux de TikTok Shop, l'algorithme, et les techniques de creation de contenu qui convertissent.",
icon: (
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"
/>
</svg>
),
weeks: "Semaines 1-2",
},
{
number: "02",
title: "Lance ton activite",
description:
"Configure ton shop, selectionne tes produits gagnants, et publie tes premieres videos avec notre methode eprouvee.",
icon: (
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M13 10V3L4 14h7v7l9-11h-7z"
/>
</svg>
),
weeks: "Semaines 3-5",
},
{
number: "03",
title: "Scale tes revenus",
description:
"Optimise tes performances, automatise tes process, et developpe une strategie de contenu rentable sur le long terme.",
icon: (
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"
/>
</svg>
),
weeks: "Semaines 6-8",
},
];
export default function Method() {
return (
<section id="methode" className="py-20 md:py-32">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center max-w-2xl mx-auto mb-16">
<span className="inline-block px-3 py-1.5 bg-primary/10 border border-primary/20 rounded-full text-primary text-xs font-medium mb-4">
La methode
</span>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-[-0.02em] mb-4">
3 etapes vers tes{" "}
<span className="gradient-text">premiers revenus</span>
</h2>
<p className="text-white/60 text-lg">
Un programme structure semaine par semaine pour te guider vers la
rentabilite.
</p>
</div>
{/* Steps */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{steps.map((step, i) => (
<Card key={i} hover className="relative">
{/* Step number */}
<div className="absolute top-6 right-6 text-5xl font-bold text-white/5">
{step.number}
</div>
{/* Icon */}
<div className="w-12 h-12 gradient-bg rounded-2xl flex items-center justify-center text-white mb-5">
{step.icon}
</div>
{/* Weeks badge */}
<span className="inline-block px-2.5 py-1 bg-primary/10 rounded-lg text-primary text-xs font-medium mb-3">
{step.weeks}
</span>
{/* Content */}
<h3 className="text-xl font-bold text-white mb-3">
{step.title}
</h3>
<p className="text-white/60 text-sm leading-relaxed">
{step.description}
</p>
</Card>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,145 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import Button from "@/components/ui/Button";
export default function Navbar() {
const [isOpen, setIsOpen] = useState(false);
return (
<nav className="fixed top-0 left-0 right-0 z-50 glass">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16 md:h-20">
{/* Logo */}
<Link href="/" className="flex items-center gap-2">
<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>
{/* Navigation desktop */}
<div className="hidden md:flex items-center gap-8">
<a
href="#methode"
className="text-white/70 hover:text-white transition-colors text-sm font-medium"
>
Methode
</a>
<a
href="#temoignages"
className="text-white/70 hover:text-white transition-colors text-sm font-medium"
>
Resultats
</a>
<a
href="#tarif"
className="text-white/70 hover:text-white transition-colors text-sm font-medium"
>
Tarif
</a>
<a
href="#faq"
className="text-white/70 hover:text-white transition-colors text-sm font-medium"
>
FAQ
</a>
</div>
{/* CTA desktop */}
<div className="hidden md:flex items-center gap-4">
<Link href="/login">
<Button variant="ghost" size="sm">
Connexion
</Button>
</Link>
<Link href="/candidature">
<Button size="sm">Candidater</Button>
</Link>
</div>
{/* Hamburger mobile */}
<button
className="md:hidden text-white p-2"
onClick={() => setIsOpen(!isOpen)}
aria-label="Menu"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{isOpen ? (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
) : (
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16M4 18h16"
/>
)}
</svg>
</button>
</div>
{/* Menu mobile */}
{isOpen && (
<div className="md:hidden pb-6 border-t border-dark-border pt-4">
<div className="flex flex-col gap-4">
<a
href="#methode"
className="text-white/70 hover:text-white transition-colors text-sm font-medium"
onClick={() => setIsOpen(false)}
>
Methode
</a>
<a
href="#temoignages"
className="text-white/70 hover:text-white transition-colors text-sm font-medium"
onClick={() => setIsOpen(false)}
>
Resultats
</a>
<a
href="#tarif"
className="text-white/70 hover:text-white transition-colors text-sm font-medium"
onClick={() => setIsOpen(false)}
>
Tarif
</a>
<a
href="#faq"
className="text-white/70 hover:text-white transition-colors text-sm font-medium"
onClick={() => setIsOpen(false)}
>
FAQ
</a>
<div className="flex flex-col gap-2 pt-2">
<Link href="/login">
<Button variant="ghost" size="sm" className="w-full">
Connexion
</Button>
</Link>
<Link href="/candidature">
<Button size="sm" className="w-full">
Candidater
</Button>
</Link>
</div>
</div>
</div>
)}
</div>
</nav>
);
}

View File

@@ -0,0 +1,100 @@
import Card from "@/components/ui/Card";
const personas = [
{
id: "jeune",
emoji: "🎓",
title: "Etudiant / Jeune actif",
subtitle: "18-25 ans",
description:
"Tu veux generer tes premiers revenus en ligne tout en etudiant ou en debut de carriere. TikTok Shop est le levier parfait.",
benefits: [
"Flexibilite totale, travaille quand tu veux",
"Pas besoin de stock ni d'investissement",
"Competences marketing valorisables sur ton CV",
"Communaute de jeunes entrepreneurs motives",
],
},
{
id: "parent",
emoji: "👨‍👩‍👧",
title: "Parent / Reconversion",
subtitle: "25-45 ans",
description:
"Tu cherches un complement de revenus ou une reconversion flexible depuis chez toi. TikTok Shop s'adapte a ton emploi du temps.",
benefits: [
"2h par jour suffisent pour demarrer",
"Travaille depuis chez toi, a ton rythme",
"Revenus complementaires des le premier mois",
"Accompagnement personnalise et bienveillant",
],
},
];
export default function PersonaCards() {
return (
<section className="py-20 md:py-32 bg-dark-light/30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center max-w-2xl mx-auto mb-16">
<span className="inline-block px-3 py-1.5 bg-primary/10 border border-primary/20 rounded-full text-primary text-xs font-medium mb-4">
Pour qui ?
</span>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-[-0.02em] mb-4">
Un programme adapte a{" "}
<span className="gradient-text">ton profil</span>
</h2>
<p className="text-white/60 text-lg">
Que tu sois etudiant ou parent, notre methode s&apos;adapte a toi.
</p>
</div>
{/* Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
{personas.map((p) => (
<Card key={p.id} hover className="relative overflow-hidden">
{/* Gradient accent top */}
<div className="absolute top-0 left-0 right-0 h-1 gradient-bg" />
<div className="pt-2">
{/* Emoji + Title */}
<div className="text-4xl mb-4">{p.emoji}</div>
<h3 className="text-xl font-bold text-white mb-1">
{p.title}
</h3>
<p className="text-primary text-sm font-medium mb-3">
{p.subtitle}
</p>
<p className="text-white/60 text-sm mb-6 leading-relaxed">
{p.description}
</p>
{/* Benefits */}
<ul className="space-y-3">
{p.benefits.map((b, i) => (
<li key={i} className="flex items-start gap-3">
<svg
className="w-5 h-5 text-success mt-0.5 shrink-0"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
<span className="text-white/70 text-sm">{b}</span>
</li>
))}
</ul>
</div>
</Card>
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,104 @@
import Link from "next/link";
import Button from "@/components/ui/Button";
import Card from "@/components/ui/Card";
const features = [
"8 semaines de coaching intensif",
"Acces a tous les modules video",
"Templates et scripts de contenu",
"Appels de groupe hebdomadaires",
"Support WhatsApp illimite",
"Communaute privee d'entrepreneurs",
"Mises a jour a vie du contenu",
"Certification HookLab",
];
export default function Pricing() {
return (
<section id="tarif" className="py-20 md:py-32">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center max-w-2xl mx-auto mb-16">
<span className="inline-block px-3 py-1.5 bg-primary/10 border border-primary/20 rounded-full text-primary text-xs font-medium mb-4">
Tarif
</span>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-[-0.02em] mb-4">
Investis dans ton{" "}
<span className="gradient-text">futur business</span>
</h2>
<p className="text-white/60 text-lg">
Un seul programme, tout inclus. Paiement en 2 fois possible.
</p>
</div>
{/* Pricing card */}
<div className="max-w-lg mx-auto">
<Card className="relative overflow-hidden border-primary/30">
{/* Popular badge */}
<div className="absolute top-0 left-0 right-0 gradient-bg py-2 text-center">
<span className="text-white text-sm font-semibold">
Offre de lancement - Places limitees
</span>
</div>
<div className="pt-12">
{/* Price */}
<div className="text-center mb-8">
<div className="flex items-baseline justify-center gap-1">
<span className="text-5xl md:text-6xl font-bold text-white">
490
</span>
<span className="text-white/40 text-lg">/mois</span>
</div>
<p className="text-white/40 mt-2">
x2 mois (980 total) - Paiement securise via Stripe
</p>
</div>
{/* Divider */}
<div className="border-t border-dark-border my-6" />
{/* Features */}
<ul className="space-y-4 mb-8">
{features.map((f, i) => (
<li key={i} className="flex items-center gap-3">
<div className="w-5 h-5 rounded-full bg-success/10 flex items-center justify-center shrink-0">
<svg
className="w-3 h-3 text-success"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M5 13l4 4L19 7"
/>
</svg>
</div>
<span className="text-white/80 text-sm">{f}</span>
</li>
))}
</ul>
{/* CTA */}
<Link href="/candidature">
<Button size="lg" className="w-full pulse-glow">
Rejoindre HookLab
</Button>
</Link>
{/* Disclaimer */}
<p className="text-center text-white/30 text-xs mt-4">
Candidature soumise a validation. Reponse sous 24h.
<br />
Satisfait ou rembourse pendant 14 jours.
</p>
</div>
</Card>
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,91 @@
import Card from "@/components/ui/Card";
const testimonials = [
{
name: "Sarah M.",
role: "Etudiante, 22 ans",
content:
"En 4 semaines, j'ai genere mes premiers 800€ sur TikTok Shop. Le programme m'a donne une methode claire et un accompagnement top.",
revenue: "2 400€/mois",
avatar: "S",
},
{
name: "Thomas D.",
role: "Ex-salarie, 34 ans",
content:
"J'hesitais a me lancer, mais le coaching m'a permis de structurer mon activite. Aujourd'hui je vis de TikTok Shop a plein temps.",
revenue: "4 200€/mois",
avatar: "T",
},
{
name: "Amina K.",
role: "Mere au foyer, 29 ans",
content:
"Je cherchais un complement de revenus flexible. Grace a HookLab, je gagne un SMIC supplementaire en travaillant 2h par jour.",
revenue: "1 600€/mois",
avatar: "A",
},
];
export default function Testimonials() {
return (
<section id="temoignages" className="py-20 md:py-32">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Header */}
<div className="text-center max-w-2xl mx-auto mb-16">
<span className="inline-block px-3 py-1.5 bg-primary/10 border border-primary/20 rounded-full text-primary text-xs font-medium mb-4">
Temoignages
</span>
<h2 className="text-3xl md:text-4xl lg:text-5xl font-bold tracking-[-0.02em] mb-4">
Ils ont <span className="gradient-text">transforme</span> leur vie
</h2>
<p className="text-white/60 text-lg">
Decouvre les resultats de nos eleves apres le programme.
</p>
</div>
{/* Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{testimonials.map((t, i) => (
<Card key={i} hover>
{/* Stars */}
<div className="flex gap-1 mb-4">
{[1, 2, 3, 4, 5].map((s) => (
<svg
key={s}
className="w-4 h-4 text-warning"
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
))}
</div>
{/* Content */}
<p className="text-white/80 mb-6 leading-relaxed">
&ldquo;{t.content}&rdquo;
</p>
{/* Revenue badge */}
<div className="inline-flex items-center px-3 py-1.5 bg-success/10 border border-success/20 rounded-full text-success text-sm font-medium mb-6">
{t.revenue}
</div>
{/* Author */}
<div className="flex items-center gap-3 pt-4 border-t border-dark-border">
<div className="w-10 h-10 rounded-full gradient-bg flex items-center justify-center text-sm font-bold text-white">
{t.avatar}
</div>
<div>
<p className="text-white font-medium text-sm">{t.name}</p>
<p className="text-white/40 text-xs">{t.role}</p>
</div>
</div>
</Card>
))}
</div>
</div>
</section>
);
}

85
components/ui/Button.tsx Normal file
View File

@@ -0,0 +1,85 @@
"use client";
import { cn } from "@/lib/utils";
import { ButtonHTMLAttributes, forwardRef } from "react";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "outline" | "ghost";
size?: "sm" | "md" | "lg";
loading?: boolean;
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
className,
variant = "primary",
size = "md",
loading = false,
disabled,
children,
...props
},
ref
) => {
const baseStyles =
"inline-flex items-center justify-center font-semibold transition-all duration-300 rounded-[12px] cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed";
const variants = {
primary:
"gradient-bg text-white hover:opacity-90 hover:translate-y-[-2px] hover:shadow-lg",
secondary:
"bg-dark-light text-white border border-dark-border hover:border-primary/50 hover:translate-y-[-2px]",
outline:
"bg-transparent text-primary border-2 border-primary hover:bg-primary hover:text-white",
ghost:
"bg-transparent text-white/70 hover:text-white hover:bg-white/5",
};
const sizes = {
sm: "px-4 py-2 text-sm",
md: "px-6 py-3 text-base",
lg: "px-8 py-4 text-lg",
};
return (
<button
ref={ref}
className={cn(baseStyles, variants[variant], sizes[size], className)}
disabled={disabled || loading}
{...props}
>
{loading ? (
<span className="flex items-center gap-2">
<svg
className="animate-spin h-4 w-4"
viewBox="0 0 24 24"
fill="none"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
Chargement...
</span>
) : (
children
)}
</button>
);
}
);
Button.displayName = "Button";
export default Button;

32
components/ui/Card.tsx Normal file
View File

@@ -0,0 +1,32 @@
import { cn } from "@/lib/utils";
import { HTMLAttributes, forwardRef } from "react";
interface CardProps extends HTMLAttributes<HTMLDivElement> {
hover?: boolean;
glass?: boolean;
}
const Card = forwardRef<HTMLDivElement, CardProps>(
({ className, hover = false, glass = false, children, ...props }, ref) => {
return (
<div
ref={ref}
className={cn(
"rounded-[20px] p-6",
glass
? "glass"
: "bg-dark-light border border-dark-border",
hover && "card-hover cursor-pointer",
className
)}
{...props}
>
{children}
</div>
);
}
);
Card.displayName = "Card";
export default Card;

77
components/ui/Input.tsx Normal file
View File

@@ -0,0 +1,77 @@
"use client";
import { cn } from "@/lib/utils";
import { InputHTMLAttributes, TextareaHTMLAttributes, forwardRef } from "react";
interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
error?: string;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, label, error, id, ...props }, ref) => {
return (
<div className="space-y-1.5">
{label && (
<label
htmlFor={id}
className="block text-sm font-medium text-white/80"
>
{label}
</label>
)}
<input
ref={ref}
id={id}
className={cn(
"w-full px-4 py-3 bg-dark-lighter border border-dark-border rounded-[12px] text-white placeholder:text-white/30 focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-colors",
error && "border-error focus:border-error focus:ring-error",
className
)}
{...props}
/>
{error && <p className="text-sm text-error">{error}</p>}
</div>
);
}
);
Input.displayName = "Input";
// Textarea séparé
interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
label?: string;
error?: string;
}
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, label, error, id, ...props }, ref) => {
return (
<div className="space-y-1.5">
{label && (
<label
htmlFor={id}
className="block text-sm font-medium text-white/80"
>
{label}
</label>
)}
<textarea
ref={ref}
id={id}
className={cn(
"w-full px-4 py-3 bg-dark-lighter border border-dark-border rounded-[12px] text-white placeholder:text-white/30 focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-colors resize-none",
error && "border-error focus:border-error focus:ring-error",
className
)}
{...props}
/>
{error && <p className="text-sm text-error">{error}</p>}
</div>
);
}
);
Textarea.displayName = "Textarea";
export { Input as default, Textarea };