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

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 };