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:
146
components/dashboard/ModuleCard.tsx
Normal file
146
components/dashboard/ModuleCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
36
components/dashboard/ProgressBar.tsx
Normal file
36
components/dashboard/ProgressBar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
156
components/dashboard/Sidebar.tsx
Normal file
156
components/dashboard/Sidebar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
110
components/marketing/FAQ.tsx
Normal file
110
components/marketing/FAQ.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
129
components/marketing/Footer.tsx
Normal file
129
components/marketing/Footer.tsx
Normal 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'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">
|
||||
© {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>
|
||||
);
|
||||
}
|
||||
91
components/marketing/Hero.tsx
Normal file
91
components/marketing/Hero.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
123
components/marketing/Method.tsx
Normal file
123
components/marketing/Method.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
145
components/marketing/Navbar.tsx
Normal file
145
components/marketing/Navbar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
100
components/marketing/PersonaCards.tsx
Normal file
100
components/marketing/PersonaCards.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
104
components/marketing/Pricing.tsx
Normal file
104
components/marketing/Pricing.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
91
components/marketing/Testimonials.tsx
Normal file
91
components/marketing/Testimonials.tsx
Normal 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">
|
||||
“{t.content}”
|
||||
</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
85
components/ui/Button.tsx
Normal 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
32
components/ui/Card.tsx
Normal 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
77
components/ui/Input.tsx
Normal 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 };
|
||||
Reference in New Issue
Block a user