fix: remove middleware and route groups to fix Vercel deployment

- Remove middleware.ts entirely (caused __dirname ReferenceError in Edge)
- Auth protection handled by dashboard layout.tsx (server-side redirect)
- Move pages out of (marketing) and (auth) route groups to fix 404 on /
- Keep (protected) route group for dashboard/formations/profil shared layout

https://claude.ai/code/session_01H2aRGDaKgarPvhay2HxN6Y
This commit is contained in:
Claude
2026-02-08 14:17:09 +00:00
parent 7a5871d344
commit 5514af9555
13 changed files with 0 additions and 156 deletions

View File

@@ -0,0 +1,133 @@
import { createClient } from "@/lib/supabase/server";
import Card from "@/components/ui/Card";
import ProgressBar from "@/components/dashboard/ProgressBar";
import ModuleCard from "@/components/dashboard/ModuleCard";
import type { Module, UserProgress, Profile } from "@/types/database.types";
export default async function DashboardPage() {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
// Récupérer le profil
const { data: profile } = await supabase
.from("profiles")
.select("*")
.eq("id", user!.id)
.single() as { data: Profile | null };
// Récupérer les modules publiés
const { data: modules } = await supabase
.from("modules")
.select("*")
.eq("is_published", true)
.order("week_number", { ascending: true })
.order("order_index", { ascending: true }) as { data: Module[] | null };
// Récupérer la progression
const { data: progress } = await supabase
.from("user_progress")
.select("*")
.eq("user_id", user!.id) as { data: UserProgress[] | null };
const totalModules = modules?.length || 0;
const completedModules =
progress?.filter((p) => p.completed).length || 0;
const progressPercent =
totalModules > 0 ? (completedModules / totalModules) * 100 : 0;
// Prochain module non complété
const completedIds = new Set(
progress?.filter((p) => p.completed).map((p) => p.module_id)
);
const nextModules =
modules?.filter((m) => !completedIds.has(m.id)).slice(0, 3) || [];
return (
<div className="max-w-6xl">
{/* Header */}
<div className="mb-10">
<h1 className="text-3xl font-bold text-white mb-2">
Bonjour {profile?.full_name?.split(" ")[0] || "!"} 👋
</h1>
<p className="text-white/60">
Voici un apercu de ta progression dans le programme.
</p>
</div>
{/* Stats cards */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-10">
<Card>
<p className="text-white/40 text-sm mb-1">Progression globale</p>
<p className="text-2xl font-bold text-white mb-3">
{Math.round(progressPercent)}%
</p>
<ProgressBar value={progressPercent} showPercentage={false} />
</Card>
<Card>
<p className="text-white/40 text-sm mb-1">Modules completes</p>
<p className="text-2xl font-bold text-white">
{completedModules}
<span className="text-white/30 text-lg font-normal">
/{totalModules}
</span>
</p>
</Card>
<Card>
<p className="text-white/40 text-sm mb-1">Statut abonnement</p>
<div className="flex items-center gap-2">
<span className="w-2 h-2 bg-success rounded-full" />
<p className="text-success font-semibold">Actif</p>
</div>
{profile?.subscription_end_date && (
<p className="text-white/30 text-xs mt-1">
Jusqu&apos;au{" "}
{new Date(profile.subscription_end_date).toLocaleDateString(
"fr-FR"
)}
</p>
)}
</Card>
</div>
{/* Prochains modules */}
{nextModules.length > 0 && (
<div>
<h2 className="text-xl font-bold text-white mb-4">
Continue ta formation
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{nextModules.map((module) => {
const moduleProgress = progress?.find(
(p) => p.module_id === module.id
);
return (
<ModuleCard
key={module.id}
module={module}
progress={moduleProgress}
/>
);
})}
</div>
</div>
)}
{/* Message si aucun module */}
{totalModules === 0 && (
<Card className="text-center py-12">
<div className="text-4xl mb-4">🚀</div>
<h3 className="text-white font-semibold text-lg mb-2">
Le programme arrive bientot !
</h3>
<p className="text-white/40 text-sm max-w-md mx-auto">
Les modules de formation sont en cours de preparation. Tu seras
notifie des qu&apos;ils seront disponibles.
</p>
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,83 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { createClient } from "@/lib/supabase/client";
import Button from "@/components/ui/Button";
interface MarkCompleteButtonProps {
moduleId: string;
userId: string;
isCompleted: boolean;
}
export default function MarkCompleteButton({
moduleId,
userId,
isCompleted: initialCompleted,
}: MarkCompleteButtonProps) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [completed, setCompleted] = useState(initialCompleted);
const handleToggle = async () => {
setLoading(true);
try {
const supabase = createClient();
if (completed) {
// Marquer comme non complété
await (supabase
.from("user_progress")
.update({ completed: false, completed_at: null } as never)
.eq("user_id", userId)
.eq("module_id", moduleId));
} else {
// Marquer comme complété (upsert)
await (supabase.from("user_progress").upsert({
user_id: userId,
module_id: moduleId,
completed: true,
completed_at: new Date().toISOString(),
} as never));
}
setCompleted(!completed);
router.refresh();
} catch (err) {
console.error("Erreur mise a jour progression:", err);
} finally {
setLoading(false);
}
};
return (
<Button
onClick={handleToggle}
loading={loading}
variant={completed ? "secondary" : "primary"}
>
{completed ? (
<span className="flex items-center gap-2">
<svg
className="w-4 h-4 text-success"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M5 13l4 4L19 7"
/>
</svg>
Complete - Annuler
</span>
) : (
"Marquer comme complete"
)}
</Button>
);
}

View File

@@ -0,0 +1,181 @@
import { redirect } from "next/navigation";
import { createClient } from "@/lib/supabase/server";
import Card from "@/components/ui/Card";
import MarkCompleteButton from "./MarkCompleteButton";
import type { Module, UserProgress } from "@/types/database.types";
interface ModulePageProps {
params: Promise<{ moduleId: string }>;
}
export default async function ModulePage({ params }: ModulePageProps) {
const { moduleId } = await params;
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
// Récupérer le module
const { data: module } = await supabase
.from("modules")
.select("*")
.eq("id", moduleId)
.eq("is_published", true)
.single() as { data: Module | null };
if (!module) {
redirect("/formations");
}
// Récupérer la progression pour ce module
const { data: progress } = await supabase
.from("user_progress")
.select("*")
.eq("user_id", user!.id)
.eq("module_id", moduleId)
.single() as { data: UserProgress | null };
return (
<div className="max-w-4xl">
{/* Breadcrumb */}
<nav className="flex items-center gap-2 text-sm mb-8">
<a
href="/formations"
className="text-white/40 hover:text-white transition-colors"
>
Formations
</a>
<span className="text-white/20">/</span>
<span className="text-white/40">Semaine {module.week_number}</span>
<span className="text-white/20">/</span>
<span className="text-white">{module.title}</span>
</nav>
{/* Header */}
<div className="mb-8">
<div className="flex items-center gap-3 mb-3">
<span className="inline-flex items-center px-2.5 py-1 bg-primary/10 rounded-lg text-primary text-xs font-medium">
Semaine {module.week_number}
</span>
{module.content_type && (
<span className="inline-flex items-center px-2.5 py-1 bg-dark-lighter rounded-lg text-white/40 text-xs font-medium uppercase">
{module.content_type}
</span>
)}
{module.duration_minutes && (
<span className="text-white/30 text-xs">
{module.duration_minutes} min
</span>
)}
{progress?.completed && (
<span className="inline-flex items-center gap-1 px-2.5 py-1 bg-success/10 rounded-lg text-success text-xs font-medium">
<svg
className="w-3 h-3"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={3}
d="M5 13l4 4L19 7"
/>
</svg>
Complete
</span>
)}
</div>
<h1 className="text-3xl font-bold text-white mb-3">{module.title}</h1>
{module.description && (
<p className="text-white/60 text-lg">{module.description}</p>
)}
</div>
{/* Contenu du module */}
<Card className="mb-8">
{/* Video */}
{module.content_type === "video" && module.content_url && (
<div className="aspect-video bg-dark-lighter rounded-2xl overflow-hidden mb-6">
<iframe
src={module.content_url}
className="w-full h-full"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
title={module.title}
/>
</div>
)}
{/* PDF */}
{module.content_type === "pdf" && module.content_url && (
<div className="mb-6">
<a
href={module.content_url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-3 bg-primary/10 text-primary rounded-xl hover:bg-primary/20 transition-colors"
>
<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 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
Telecharger le PDF
</a>
</div>
)}
{/* Placeholder si pas de contenu */}
{!module.content_url && (
<div className="aspect-video bg-dark-lighter rounded-2xl flex items-center justify-center mb-6">
<div className="text-center">
<div className="text-4xl mb-3">🎬</div>
<p className="text-white/40 text-sm">
Le contenu sera bientot disponible
</p>
</div>
</div>
)}
</Card>
{/* Actions */}
<div className="flex items-center justify-between">
<a
href="/formations"
className="text-white/40 hover:text-white text-sm transition-colors flex items-center gap-1"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Retour aux formations
</a>
<MarkCompleteButton
moduleId={moduleId}
userId={user!.id}
isCompleted={progress?.completed || false}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,108 @@
import { createClient } from "@/lib/supabase/server";
import ModuleCard from "@/components/dashboard/ModuleCard";
import ProgressBar from "@/components/dashboard/ProgressBar";
import type { Module, UserProgress } from "@/types/database.types";
export default async function FormationsPage() {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
// Récupérer les modules publiés
const { data: modules } = await supabase
.from("modules")
.select("*")
.eq("is_published", true)
.order("week_number", { ascending: true })
.order("order_index", { ascending: true }) as { data: Module[] | null };
// Récupérer la progression
const { data: progress } = await supabase
.from("user_progress")
.select("*")
.eq("user_id", user!.id) as { data: UserProgress[] | null };
// Grouper les modules par semaine
const modulesByWeek = (modules || []).reduce(
(acc, module) => {
const week = module.week_number;
if (!acc[week]) acc[week] = [];
acc[week].push(module);
return acc;
},
{} as Record<number, Module[]>
);
const totalModules = modules?.length || 0;
const completedModules =
progress?.filter((p) => p.completed).length || 0;
const progressPercent =
totalModules > 0 ? (completedModules / totalModules) * 100 : 0;
return (
<div className="max-w-6xl">
{/* Header */}
<div className="mb-10">
<h1 className="text-3xl font-bold text-white mb-2">Formations</h1>
<p className="text-white/60 mb-6">
Progression dans le programme HookLab - 8 semaines.
</p>
<ProgressBar
value={progressPercent}
label={`${completedModules} modules completes sur ${totalModules}`}
/>
</div>
{/* Modules par semaine */}
{Object.entries(modulesByWeek).map(([week, weekModules]) => {
const weekCompleted =
weekModules?.filter((m) =>
progress?.find((p) => p.module_id === m.id && p.completed)
).length || 0;
const weekTotal = weekModules?.length || 0;
return (
<div key={week} className="mb-10">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-white">
Semaine {week}
</h2>
<span className="text-white/30 text-sm">
{weekCompleted}/{weekTotal} completes
</span>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{weekModules?.map((module) => {
const moduleProgress = progress?.find(
(p) => p.module_id === module.id
);
return (
<ModuleCard
key={module.id}
module={module}
progress={moduleProgress}
/>
);
})}
</div>
</div>
);
})}
{/* Message si aucun module */}
{totalModules === 0 && (
<div className="text-center py-20">
<div className="text-5xl mb-4">📚</div>
<h3 className="text-white font-semibold text-lg mb-2">
Aucun module disponible
</h3>
<p className="text-white/40 text-sm">
Les modules de formation seront bientot disponibles.
</p>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,44 @@
import { redirect } from "next/navigation";
import { createClient } from "@/lib/supabase/server";
import Sidebar from "@/components/dashboard/Sidebar";
import type { Profile } from "@/types/database.types";
export default async function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const supabase = await createClient();
// Vérifier l'authentification
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
redirect("/login");
}
// Récupérer le profil
const { data: profile } = await supabase
.from("profiles")
.select("*")
.eq("id", user.id)
.single() as { data: Profile | null };
if (!profile) {
redirect("/login");
}
// Vérifier l'abonnement actif
if (profile.subscription_status !== "active") {
redirect("/login");
}
return (
<div className="flex min-h-screen">
<Sidebar user={profile} />
<main className="flex-1 p-6 md:p-10 overflow-y-auto">{children}</main>
</div>
);
}

View File

@@ -0,0 +1,240 @@
"use client";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { createClient } from "@/lib/supabase/client";
import Button from "@/components/ui/Button";
import Input from "@/components/ui/Input";
import Card from "@/components/ui/Card";
import type { Profile } from "@/types/database.types";
export default function ProfilPage() {
const router = useRouter();
const [profile, setProfile] = useState<Profile | null>(null);
const [fullName, setFullName] = useState("");
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState<{
type: "success" | "error";
text: string;
} | null>(null);
// Changement de mot de passe
const [newPassword, setNewPassword] = useState("");
const [confirmNewPassword, setConfirmNewPassword] = useState("");
const [passwordSaving, setPasswordSaving] = useState(false);
useEffect(() => {
const loadProfile = async () => {
setLoading(true);
const supabase = createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) return;
const { data } = await supabase
.from("profiles")
.select("*")
.eq("id", user.id)
.single() as { data: Profile | null };
if (data) {
setProfile(data);
setFullName(data.full_name || "");
}
setLoading(false);
};
loadProfile();
}, []);
const handleSaveProfile = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
setMessage(null);
try {
const supabase = createClient();
const { error } = await supabase
.from("profiles")
.update({ full_name: fullName } as never)
.eq("id", profile!.id);
if (error) throw error;
setMessage({ type: "success", text: "Profil mis a jour !" });
router.refresh();
} catch {
setMessage({
type: "error",
text: "Erreur lors de la mise a jour du profil.",
});
} finally {
setSaving(false);
}
};
const handleChangePassword = async (e: React.FormEvent) => {
e.preventDefault();
setPasswordSaving(true);
setMessage(null);
if (newPassword !== confirmNewPassword) {
setMessage({
type: "error",
text: "Les mots de passe ne correspondent pas.",
});
setPasswordSaving(false);
return;
}
if (newPassword.length < 8) {
setMessage({
type: "error",
text: "Le mot de passe doit contenir au moins 8 caracteres.",
});
setPasswordSaving(false);
return;
}
try {
const supabase = createClient();
const { error } = await supabase.auth.updateUser({
password: newPassword,
});
if (error) throw error;
setMessage({ type: "success", text: "Mot de passe mis a jour !" });
setNewPassword("");
setConfirmNewPassword("");
} catch {
setMessage({
type: "error",
text: "Erreur lors du changement de mot de passe.",
});
} finally {
setPasswordSaving(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin w-8 h-8 border-2 border-primary border-t-transparent rounded-full" />
</div>
);
}
return (
<div className="max-w-2xl">
<h1 className="text-3xl font-bold text-white mb-2">Mon profil</h1>
<p className="text-white/60 mb-10">
Gere tes informations personnelles et ton abonnement.
</p>
{/* Message */}
{message && (
<div
className={`mb-6 p-3 rounded-xl border ${
message.type === "success"
? "bg-success/10 border-success/20 text-success"
: "bg-error/10 border-error/20 text-error"
}`}
>
<p className="text-sm">{message.text}</p>
</div>
)}
{/* Informations profil */}
<Card className="mb-6">
<h2 className="text-lg font-semibold text-white mb-6">
Informations personnelles
</h2>
<form onSubmit={handleSaveProfile} className="space-y-5">
<Input
id="email"
label="Email"
type="email"
value={profile?.email || ""}
disabled
className="opacity-50"
/>
<Input
id="fullName"
label="Nom complet"
placeholder="Jean Dupont"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
/>
<Button type="submit" loading={saving}>
Sauvegarder
</Button>
</form>
</Card>
{/* Changement de mot de passe */}
<Card className="mb-6">
<h2 className="text-lg font-semibold text-white mb-6">
Changer le mot de passe
</h2>
<form onSubmit={handleChangePassword} className="space-y-5">
<Input
id="newPassword"
label="Nouveau mot de passe"
type="password"
placeholder="Minimum 8 caracteres"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
<Input
id="confirmNewPassword"
label="Confirmer le mot de passe"
type="password"
placeholder="Confirme ton nouveau mot de passe"
value={confirmNewPassword}
onChange={(e) => setConfirmNewPassword(e.target.value)}
/>
<Button type="submit" loading={passwordSaving} variant="secondary">
Changer le mot de passe
</Button>
</form>
</Card>
{/* Abonnement */}
<Card>
<h2 className="text-lg font-semibold text-white mb-4">Abonnement</h2>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-white/60 text-sm">Statut</span>
<span className="inline-flex items-center gap-1.5 px-2.5 py-1 bg-success/10 rounded-lg text-success text-sm font-medium">
<span className="w-1.5 h-1.5 bg-success rounded-full" />
{profile?.subscription_status === "active"
? "Actif"
: "Inactif"}
</span>
</div>
{profile?.subscription_end_date && (
<div className="flex items-center justify-between">
<span className="text-white/60 text-sm">Valide jusqu&apos;au</span>
<span className="text-white text-sm">
{new Date(profile.subscription_end_date).toLocaleDateString(
"fr-FR",
{ day: "numeric", month: "long", year: "numeric" }
)}
</span>
</div>
)}
<div className="flex items-center justify-between">
<span className="text-white/60 text-sm">Plan</span>
<span className="text-white text-sm">
HookLab - Programme 8 semaines
</span>
</div>
</div>
</Card>
</div>
);
}