feat: pump tracker, photo log, milk stock, medication profiles, calendar, reminders, audit log, edit events
New pages: - /calendar — monthly grid with event dots, day detail on tap - /medications — CRUD for medication profiles (presets + custom, dose/unit/intervals/crisis mode) - /milk — milk stock management with location-based expiry, alerts, mark-as-used - /reminders — recurring medication reminder CRUD with Pushover notifications New APIs: - /api/medication-profiles — profiles CRUD + last-intakes endpoint (auto-seeds 10 presets per family) - /api/milk — milk stock CRUD with auto-calculated expiry - /api/reminders + /api/reminders/check — reminder CRUD + cron-callable check endpoint - /api/upload — multipart photo upload to public/uploads/ - /api/invite/send-email — email invitation with family invite link - /api/admin/audit — last 100 audit log entries (superadmin only) Event modal improvements: - PUMP event type (side selector + volume + timer) - MEDICATION: profile chip picker, next-dose timing status, crisis mode toggle - Photo attachment (upload + thumbnail preview) - Datetime inputs split into date + time (iOS Safari datetime-local fix) - Edit mode via initialEvent prop (pre-fills all fields, saves via PATCH) - Timer now shows h:mm:ss when >= 1 hour Timeline: - Modify button opens pre-filled edit modal per event - Photo thumbnail in expanded view Dashboard: - PUMP in quick-log - Medication status card (too-soon / available with next-dose time) Schema additions: MedicationProfile, MedcationReminder, MilkStock, AuditLog models Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.email) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
if (session.user.email !== process.env.SUPERADMIN_EMAIL) {
|
||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||
}
|
||||
|
||||
const logs = await prisma.auditLog.findMany({
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 100,
|
||||
});
|
||||
|
||||
return NextResponse.json(logs);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { logAudit } from "@/lib/audit";
|
||||
|
||||
export async function PATCH(
|
||||
req: Request,
|
||||
@@ -25,6 +26,7 @@ export async function PATCH(
|
||||
include: { user: { select: { id: true, name: true } } },
|
||||
});
|
||||
|
||||
await logAudit(session, { action: "event.update", targetId: id });
|
||||
return NextResponse.json(event);
|
||||
}
|
||||
|
||||
@@ -37,5 +39,6 @@ export async function DELETE(
|
||||
|
||||
const { id } = await params;
|
||||
await prisma.event.delete({ where: { id } });
|
||||
await logAudit(session, { action: "event.delete", targetId: id });
|
||||
return new NextResponse(null, { status: 204 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { logAudit } from "@/lib/audit";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const session = await auth();
|
||||
@@ -63,5 +64,7 @@ export async function POST(req: Request) {
|
||||
include: { user: { select: { id: true, name: true } } },
|
||||
});
|
||||
|
||||
await logAudit(session, { action: "event.create", targetId: event.id, detail: `${type} pour bébé ${babyId}` });
|
||||
|
||||
return NextResponse.json(event, { status: 201 });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { sendEmail } from "@/lib/email";
|
||||
|
||||
const VALID_ROLES = ["PARENT", "READONLY", "DOCTOR"] as const;
|
||||
type Role = (typeof VALID_ROLES)[number];
|
||||
|
||||
const ROLE_LABELS: Record<Role, string> = {
|
||||
PARENT: "Parent",
|
||||
READONLY: "Lecture seule",
|
||||
DOCTOR: "Médecin",
|
||||
};
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const { email, role } = await req.json();
|
||||
if (!email || !role || !VALID_ROLES.includes(role)) {
|
||||
return NextResponse.json({ error: "Champs requis manquants" }, { status: 400 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
include: { family: true },
|
||||
});
|
||||
|
||||
if (!user?.family) {
|
||||
return NextResponse.json({ error: "Famille introuvable" }, { status: 404 });
|
||||
}
|
||||
|
||||
const origin = req.headers.get("origin") ?? process.env.NEXTAUTH_URL ?? "";
|
||||
const inviteUrl = `${origin}/auth/invite/${user.family.inviteCode}?role=${role}`;
|
||||
const assignedRole = role as Role;
|
||||
|
||||
try {
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: `Invitation Grow — ${user.family.name}`,
|
||||
html: `
|
||||
<div style="font-family:sans-serif;max-width:480px;margin:0 auto">
|
||||
<h2 style="color:#4f46e5">Grow — Invitation</h2>
|
||||
<p><strong>${user.name}</strong> vous invite à rejoindre la famille <strong>${user.family.name}</strong> sur Grow.</p>
|
||||
<p>Votre rôle : <strong>${ROLE_LABELS[assignedRole]}</strong></p>
|
||||
<a href="${inviteUrl}" style="display:inline-block;margin:16px 0;padding:12px 24px;background:#4f46e5;color:#fff;border-radius:8px;text-decoration:none;font-weight:600">
|
||||
Rejoindre la famille
|
||||
</a>
|
||||
<p style="color:#64748b;font-size:13px">Ou copiez ce lien : ${inviteUrl}</p>
|
||||
</div>
|
||||
`,
|
||||
text: `${user.name} vous invite à rejoindre ${user.family.name} sur Grow.\n\nRôle : ${ROLE_LABELS[assignedRole]}\n\nLien : ${inviteUrl}`,
|
||||
});
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Envoi email impossible — vérifiez la config email." }, { status: 500 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function PATCH(
|
||||
req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const body = await req.json();
|
||||
|
||||
const profile = await prisma.medicationProfile.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(body.name !== undefined ? { name: body.name } : {}),
|
||||
...(body.molecule !== undefined ? { molecule: body.molecule } : {}),
|
||||
...(body.defaultDose !== undefined ? { defaultDose: body.defaultDose } : {}),
|
||||
...(body.unit !== undefined ? { unit: body.unit } : {}),
|
||||
...(body.intervalHours !== undefined ? { intervalHours: parseFloat(body.intervalHours) } : {}),
|
||||
...(body.minIntervalHours !== undefined ? { minIntervalHours: body.minIntervalHours ? parseFloat(body.minIntervalHours) : null } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(profile);
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
await prisma.medicationProfile.delete({ where: { id } });
|
||||
return new NextResponse(null, { status: 204 });
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
// Returns the most recent MEDICATION event per medication name for a given baby.
|
||||
// Used to compute next-allowed-dose times in the UI.
|
||||
export async function GET(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const babyId = searchParams.get("babyId");
|
||||
if (!babyId) return NextResponse.json({ error: "babyId requis" }, { status: 400 });
|
||||
|
||||
// Fetch last 100 medication events — enough to find latest per med
|
||||
const events = await prisma.event.findMany({
|
||||
where: { babyId, type: "MEDICATION" },
|
||||
orderBy: { startedAt: "desc" },
|
||||
take: 100,
|
||||
select: { id: true, startedAt: true, metadata: true },
|
||||
});
|
||||
|
||||
// Deduplicate: keep only the latest per medication name
|
||||
const latest: Record<string, { id: string; startedAt: Date; dose?: string; unit?: string }> = {};
|
||||
for (const ev of events) {
|
||||
const meta = ev.metadata as Record<string, unknown> | null;
|
||||
const name = String(meta?.name ?? "");
|
||||
if (name && !latest[name]) {
|
||||
latest[name] = {
|
||||
id: ev.id,
|
||||
startedAt: ev.startedAt,
|
||||
dose: meta?.dose ? String(meta.dose) : undefined,
|
||||
unit: meta?.unit ? String(meta.unit) : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json(latest);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { MEDICATION_PRESETS } from "@/lib/medication-presets";
|
||||
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id }, select: { familyId: true } });
|
||||
if (!user?.familyId) return NextResponse.json([]);
|
||||
|
||||
const profiles = await prisma.medicationProfile.findMany({
|
||||
where: { familyId: user.familyId },
|
||||
orderBy: [{ isPreset: "desc" }, { name: "asc" }],
|
||||
});
|
||||
|
||||
// Auto-seed presets for this family if none exist yet
|
||||
if (profiles.length === 0) {
|
||||
await prisma.medicationProfile.createMany({
|
||||
data: MEDICATION_PRESETS.map((p) => ({
|
||||
familyId: user.familyId!,
|
||||
name: p.name,
|
||||
molecule: p.molecule,
|
||||
defaultDose: p.defaultDose ?? null,
|
||||
unit: p.unit,
|
||||
intervalHours: p.intervalHours,
|
||||
minIntervalHours: p.minIntervalHours,
|
||||
isPreset: true,
|
||||
})),
|
||||
});
|
||||
const seeded = await prisma.medicationProfile.findMany({
|
||||
where: { familyId: user.familyId },
|
||||
orderBy: [{ isPreset: "desc" }, { name: "asc" }],
|
||||
});
|
||||
return NextResponse.json(seeded);
|
||||
}
|
||||
|
||||
return NextResponse.json(profiles);
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id }, select: { familyId: true } });
|
||||
if (!user?.familyId) return NextResponse.json({ error: "Famille introuvable" }, { status: 400 });
|
||||
|
||||
const body = await req.json();
|
||||
const { name, molecule, defaultDose, unit, intervalHours, minIntervalHours } = body;
|
||||
if (!name || !intervalHours) return NextResponse.json({ error: "Champs requis manquants" }, { status: 400 });
|
||||
|
||||
const profile = await prisma.medicationProfile.create({
|
||||
data: {
|
||||
familyId: user.familyId,
|
||||
name,
|
||||
molecule: molecule ?? null,
|
||||
defaultDose: defaultDose ?? null,
|
||||
unit: unit ?? "mL",
|
||||
intervalHours: parseFloat(intervalHours),
|
||||
minIntervalHours: minIntervalHours ? parseFloat(minIntervalHours) : null,
|
||||
isPreset: false,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(profile, { status: 201 });
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function PATCH(
|
||||
req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const body = await req.json();
|
||||
|
||||
const data: Record<string, unknown> = {};
|
||||
if (body.volume !== undefined) data.volume = parseFloat(body.volume);
|
||||
if (body.notes !== undefined) data.notes = body.notes;
|
||||
if (body.location !== undefined) data.location = body.location;
|
||||
if (body.expiresAt !== undefined) data.expiresAt = new Date(body.expiresAt);
|
||||
if (body.used !== undefined) {
|
||||
data.used = body.used;
|
||||
data.usedAt = body.used ? new Date() : null;
|
||||
}
|
||||
|
||||
const stock = await prisma.milkStock.update({ where: { id }, data });
|
||||
return NextResponse.json(stock);
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
await prisma.milkStock.delete({ where: { id } });
|
||||
return new NextResponse(null, { status: 204 });
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
// Default expiry hours by location
|
||||
const EXPIRY_HOURS: Record<string, number> = {
|
||||
room: 4,
|
||||
fridge: 96, // 4 days
|
||||
freezer: 4320, // 6 months
|
||||
};
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const babyId = searchParams.get("babyId");
|
||||
if (!babyId) return NextResponse.json({ error: "babyId requis" }, { status: 400 });
|
||||
|
||||
const stocks = await prisma.milkStock.findMany({
|
||||
where: { babyId },
|
||||
orderBy: { storedAt: "desc" },
|
||||
});
|
||||
|
||||
return NextResponse.json(stocks);
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const body = await req.json();
|
||||
const { babyId, volume, storedAt, location, notes, expiryHours } = body;
|
||||
|
||||
if (!babyId || !volume) {
|
||||
return NextResponse.json({ error: "Champs requis manquants" }, { status: 400 });
|
||||
}
|
||||
|
||||
const loc = location ?? "fridge";
|
||||
const storedDate = storedAt ? new Date(storedAt) : new Date();
|
||||
const hours = expiryHours ?? EXPIRY_HOURS[loc] ?? EXPIRY_HOURS.fridge;
|
||||
const expiresAt = new Date(storedDate.getTime() + hours * 60 * 60 * 1000);
|
||||
|
||||
const stock = await prisma.milkStock.create({
|
||||
data: {
|
||||
babyId,
|
||||
volume: parseFloat(volume),
|
||||
storedAt: storedDate,
|
||||
expiresAt,
|
||||
location: loc,
|
||||
notes: notes ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(stock, { status: 201 });
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function PATCH(
|
||||
req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
const body = await req.json();
|
||||
|
||||
const reminder = await prisma.medicationReminder.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(body.name !== undefined ? { name: body.name } : {}),
|
||||
...(body.dose !== undefined ? { dose: body.dose } : {}),
|
||||
...(body.unit !== undefined ? { unit: body.unit } : {}),
|
||||
...(body.intervalHours !== undefined ? { intervalHours: parseFloat(body.intervalHours) } : {}),
|
||||
...(body.startAt !== undefined ? { startAt: new Date(body.startAt) } : {}),
|
||||
...(body.enabled !== undefined ? { enabled: body.enabled } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(reminder);
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_req: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const { id } = await params;
|
||||
await prisma.medicationReminder.delete({ where: { id } });
|
||||
return new NextResponse(null, { status: 204 });
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { notifyFamilyFeedAlert } from "@/lib/push-notify";
|
||||
|
||||
export async function POST() {
|
||||
const now = new Date();
|
||||
|
||||
const reminders = await prisma.medicationReminder.findMany({
|
||||
where: { enabled: true },
|
||||
include: { baby: { include: { family: true } } },
|
||||
});
|
||||
|
||||
let sent = 0;
|
||||
|
||||
for (const reminder of reminders) {
|
||||
const intervalMs = reminder.intervalHours * 60 * 60 * 1000;
|
||||
const nextDue = new Date(
|
||||
(reminder.lastSentAt ?? reminder.startAt).getTime() + intervalMs
|
||||
);
|
||||
|
||||
if (now >= nextDue) {
|
||||
const dosePart = reminder.dose ? ` — ${reminder.dose}${reminder.unit ? " " + reminder.unit : ""}` : "";
|
||||
const message = `Rappel médicament : ${reminder.name}${dosePart} pour ${reminder.baby.name}`;
|
||||
|
||||
try {
|
||||
await notifyFamilyFeedAlert(reminder.baby.familyId, message);
|
||||
await prisma.medicationReminder.update({
|
||||
where: { id: reminder.id },
|
||||
data: { lastSentAt: now },
|
||||
});
|
||||
sent++;
|
||||
} catch {
|
||||
// continue with other reminders even if one fails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ checked: reminders.length, sent });
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const babyId = searchParams.get("babyId");
|
||||
if (!babyId) return NextResponse.json({ error: "babyId requis" }, { status: 400 });
|
||||
|
||||
const reminders = await prisma.medicationReminder.findMany({
|
||||
where: { babyId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
return NextResponse.json(reminders);
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const body = await req.json();
|
||||
const { babyId, name, dose, unit, intervalHours, startAt } = body;
|
||||
|
||||
if (!babyId || !name || !intervalHours || !startAt) {
|
||||
return NextResponse.json({ error: "Champs requis manquants" }, { status: 400 });
|
||||
}
|
||||
|
||||
const reminder = await prisma.medicationReminder.create({
|
||||
data: {
|
||||
babyId,
|
||||
name,
|
||||
dose: dose ?? null,
|
||||
unit: unit ?? null,
|
||||
intervalHours: parseFloat(intervalHours),
|
||||
startAt: new Date(startAt),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(reminder, { status: 201 });
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { writeFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp", "image/gif"];
|
||||
const MAX_SIZE = 5 * 1024 * 1024; // 5 MB
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
|
||||
const formData = await req.formData();
|
||||
const file = formData.get("file") as File | null;
|
||||
|
||||
if (!file) return NextResponse.json({ error: "Fichier manquant" }, { status: 400 });
|
||||
if (!ALLOWED_TYPES.includes(file.type)) return NextResponse.json({ error: "Type de fichier non supporté" }, { status: 400 });
|
||||
if (file.size > MAX_SIZE) return NextResponse.json({ error: "Fichier trop volumineux (max 5 MB)" }, { status: 400 });
|
||||
|
||||
const ext = file.type.split("/")[1].replace("jpeg", "jpg");
|
||||
const filename = `${randomUUID()}.${ext}`;
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
|
||||
const uploadDir = join(process.cwd(), "public", "uploads");
|
||||
await writeFile(join(uploadDir, filename), buffer);
|
||||
|
||||
return NextResponse.json({ url: `/uploads/${filename}` });
|
||||
}
|
||||
Reference in New Issue
Block a user