Initial commit — Grow baby tracker

Next.js 16 App Router, Prisma + PostgreSQL, NextAuth v5 JWT.

Features: dashboard quick-log, timeline, growth charts (WHO percentiles),
stats, journal/notes, doctor notes, milestones, vaccinations, settings,
superadmin panel. Mobile-first with sidebar nav + bottom nav + quick-add FAB.
Dark mode, PWA push notifications, multi-family invite system.

Docker: multi-stage Dockerfile + docker-compose with postgres service.
This commit is contained in:
2026-06-13 01:52:46 +02:00
commit cd16c354c0
94 changed files with 14354 additions and 0 deletions
+78
View File
@@ -0,0 +1,78 @@
import { prisma } from "./prisma";
interface EmailPayload {
to: string;
subject: string;
html: string;
text?: string;
}
async function getConfig(key: string): Promise<string> {
const row = await prisma.systemConfig.findUnique({ where: { key } });
return row?.value ?? process.env[key.toUpperCase()] ?? "";
}
async function sendMailgun(payload: EmailPayload): Promise<void> {
const apiKey = await getConfig("mailgun_api_key");
const domain = await getConfig("mailgun_domain");
const from = await getConfig("mailgun_from");
if (!apiKey || !domain) throw new Error("Mailgun non configuré");
const formData = new FormData();
formData.append("from", from || `Grow <noreply@${domain}>`);
formData.append("to", payload.to);
formData.append("subject", payload.subject);
formData.append("html", payload.html);
if (payload.text) formData.append("text", payload.text);
const res = await fetch(`https://api.mailgun.net/v3/${domain}/messages`, {
method: "POST",
headers: { Authorization: `Basic ${Buffer.from(`api:${apiKey}`).toString("base64")}` },
body: formData,
});
if (!res.ok) {
const err = await res.text();
throw new Error(`Mailgun error: ${err}`);
}
}
async function sendSmtp(payload: EmailPayload): Promise<void> {
const host = await getConfig("smtp_host");
const port = parseInt(await getConfig("smtp_port") || "587");
const user = await getConfig("smtp_user");
const pass = await getConfig("smtp_pass");
const from = await getConfig("smtp_from");
if (!host) throw new Error("SMTP non configuré");
const nodemailer = await import("nodemailer");
const transport = nodemailer.default.createTransport({ host, port, auth: { user, pass } });
await transport.sendMail({ from, to: payload.to, subject: payload.subject, html: payload.html, text: payload.text });
}
export async function sendEmail(payload: EmailPayload): Promise<void> {
const provider = await getConfig("email_provider");
if (provider === "smtp") return sendSmtp(payload);
if (provider === "mailgun") return sendMailgun(payload);
// Fallback: check env vars directly
if (process.env.MAILGUN_API_KEY) return sendMailgun(payload);
if (process.env.SMTP_HOST) return sendSmtp(payload);
throw new Error("Aucun service email configuré");
}
export function resetPasswordEmail(name: string, resetUrl: string): { html: string; text: string } {
return {
text: `Bonjour ${name},\n\nCliquez sur ce lien pour réinitialiser votre mot de passe :\n${resetUrl}\n\nLien valide 1 heure.\n\nGrow`,
html: `
<div style="font-family:sans-serif;max-width:480px;margin:0 auto">
<h2 style="color:#4f46e5">Grow — Réinitialisation du mot de passe</h2>
<p>Bonjour ${name},</p>
<p>Cliquez sur le bouton ci-dessous pour réinitialiser votre mot de passe. Le lien est valide pendant <strong>1 heure</strong>.</p>
<a href="${resetUrl}" style="display:inline-block;margin:16px 0;padding:12px 24px;background:#4f46e5;color:#fff;border-radius:8px;text-decoration:none;font-weight:600">
Réinitialiser mon mot de passe
</a>
<p style="color:#64748b;font-size:13px">Si vous n'avez pas demandé cette réinitialisation, ignorez cet email.</p>
<p style="color:#64748b;font-size:13px">Ou copiez ce lien : ${resetUrl}</p>
</div>
`,
};
}