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

6
lib/stripe/client.ts Normal file
View File

@@ -0,0 +1,6 @@
import Stripe from "stripe";
// Client Stripe côté serveur
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
typescript: true,
});

9
lib/supabase/client.ts Normal file
View File

@@ -0,0 +1,9 @@
import { createBrowserClient } from "@supabase/ssr";
import type { Database } from "@/types/database.types";
// Client Supabase côté navigateur (composants client)
export const createClient = () =>
createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

View File

@@ -0,0 +1,59 @@
import { createServerClient } from "@supabase/ssr";
import { NextResponse, type NextRequest } from "next/server";
// Middleware Supabase pour refresh des tokens auth
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request,
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value)
);
supabaseResponse = NextResponse.next({
request,
});
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
);
},
},
}
);
const {
data: { user },
} = await supabase.auth.getUser();
// Rediriger vers login si pas connecté et route protégée
if (
!user &&
request.nextUrl.pathname.startsWith("/dashboard")
) {
const url = request.nextUrl.clone();
url.pathname = "/login";
return NextResponse.redirect(url);
}
// Rediriger vers dashboard si déjà connecté et sur login/register
if (
user &&
(request.nextUrl.pathname === "/login" ||
request.nextUrl.pathname === "/register")
) {
const url = request.nextUrl.clone();
url.pathname = "/dashboard";
return NextResponse.redirect(url);
}
return supabaseResponse;
}

45
lib/supabase/server.ts Normal file
View File

@@ -0,0 +1,45 @@
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import type { Database } from "@/types/database.types";
// Client Supabase côté serveur (Server Components, Route Handlers)
export const createClient = async () => {
const cookieStore = await cookies();
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch {
// Ignore en Server Component (lecture seule)
}
},
},
}
);
};
// Client admin avec service role (webhooks, opérations admin)
export const createAdminClient = () => {
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!,
{
cookies: {
getAll() {
return [];
},
setAll() {},
},
}
);
};

28
lib/utils.ts Normal file
View File

@@ -0,0 +1,28 @@
import { type ClassValue, clsx } from "clsx";
// Utilitaire pour combiner les classes CSS (compatible Tailwind)
export function cn(...inputs: ClassValue[]) {
return inputs.filter(Boolean).join(" ");
}
// Formater un prix en euros
export function formatPrice(amount: number): string {
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(amount / 100);
}
// Valider un email
export function isValidEmail(email: string): boolean {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(email);
}
// URL de base de l'application
export function getBaseUrl(): string {
if (process.env.NEXT_PUBLIC_APP_URL) {
return process.env.NEXT_PUBLIC_APP_URL;
}
return "http://localhost:3000";
}