feat: Initial Diabetix application commit

- Add authentication with NextAuth v5 (credentials + email verification)
- Implement dashboard with glycemia tracking and AI analysis
- Add PDF report generation for Premium users
- Implement Stripe integration for Premium subscriptions
- Add responsive UI with Tailwind CSS and shadcn components
- Database schema with Prisma ORM and PostgreSQL support
- Real-time glycemia visualization with Recharts
- Mobile-optimized entry form
- User profile management with medical information
- Subscription lifecycle management (create, cancel, webhook)
- Email notifications with Resend
- Feature gates for Free vs Premium plans

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jeremy bayse
2026-04-26 23:06:29 +02:00
parent a0821ce83c
commit e7f151d14e
84 changed files with 8417 additions and 116 deletions

View File

@@ -0,0 +1,110 @@
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { format, subMonths, startOfMonth, endOfMonth } from "date-fns";
import { fr } from "date-fns/locale";
import { Download, Lock } from "lucide-react";
export const dynamic = "force-dynamic";
export const metadata = { title: "Mes Rapports — Diabetix" };
export default async function RapportsPage() {
const session = await auth();
if (!session?.user?.id) redirect("/auth/login");
const userId = session.user.id;
const user = await prisma.user.findUnique({ where: { id: userId } });
const isPremium = user?.plan === "PREMIUM";
const months = Array.from({ length: 12 }, (_, i) => {
return subMonths(startOfMonth(new Date()), i);
});
const readingsByMonth = await Promise.all(
months.map(async (month) => {
const count = await prisma.reading.count({
where: {
userId,
measuredAt: {
gte: month,
lte: endOfMonth(month),
},
},
});
return { month, count };
})
);
// Filter to only show months with readings
const monthsWithReadings = readingsByMonth.filter(({ count }) => count > 0);
return (
<div className="mx-auto max-w-5xl space-y-6">
<div>
<h1 className="text-2xl font-semibold text-slate-900">Mes Rapports</h1>
<p className="mt-1 text-sm text-slate-500">
Téléchargez vos rapports glycémie mensuels en PDF
</p>
</div>
{!isPremium && (
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4">
<div className="flex gap-2">
<Lock className="h-5 w-5 text-blue-600 flex-shrink-0" />
<div className="text-sm text-blue-800">
<p className="font-medium">Feature Premium</p>
<p className="text-blue-700">
Les rapports PDF sont exclusifs au plan Premium. Passer à Premium pour y accéder.
</p>
</div>
</div>
</div>
)}
<div className="space-y-2">
{monthsWithReadings.length > 0 ? (
monthsWithReadings.map(({ month, count }) => (
<div
key={month.toISOString()}
className="flex items-center justify-between rounded-lg border border-slate-200 bg-white p-4 hover:border-slate-300"
>
<div>
<p className="font-medium text-slate-900">
{format(month, "MMMM yyyy", { locale: fr })}
</p>
<p className="text-sm text-slate-500">{count} mesure(s)</p>
</div>
{count > 0 && isPremium && (
<a
href={`/api/reports/generate-pdf?month=${month.toISOString()}`}
download
className="inline-flex items-center gap-2 rounded-lg bg-teal-600 px-3 py-2 text-sm font-medium text-white hover:bg-teal-700"
>
<Download className="h-4 w-4" />
<span className="hidden sm:inline">Télécharger</span>
<span className="sm:hidden">PDF</span>
</a>
)}
{count > 0 && !isPremium && (
<div className="inline-flex items-center gap-2 rounded-lg bg-slate-100 px-3 py-2 text-sm font-medium text-slate-500">
<Lock className="h-4 w-4" />
</div>
)}
{count === 0 && (
<p className="text-sm text-slate-400">Aucune donnée</p>
)}
</div>
))
) : (
<div className="text-center rounded-lg border border-slate-200 bg-white p-8">
<p className="text-sm text-slate-500">Aucun mois avec des données.</p>
</div>
)}
</div>
</div>
);
}