Marketplace inversée (Next.js 16 App Router + Supabase Cloud) : auth, schéma + RLS, publication de besoins avec upload d'images, feed avec filtres, offres, acceptation via RPC, messagerie temps réel, dashboards, et Dockerfile pour déploiement Coolify.
98 lines
2.7 KiB
TypeScript
98 lines
2.7 KiB
TypeScript
"use server";
|
|
|
|
import { redirect } from "next/navigation";
|
|
import { createClient } from "@/lib/supabase/server";
|
|
import { verifySession } from "@/lib/dal";
|
|
import { NeedFormSchema, type NeedFormState } from "@/lib/validations/needs";
|
|
|
|
const MAX_IMAGES = 5;
|
|
const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5 Mo
|
|
|
|
export async function createNeed(
|
|
_state: NeedFormState,
|
|
formData: FormData
|
|
): Promise<NeedFormState> {
|
|
const user = await verifySession();
|
|
|
|
const validatedFields = NeedFormSchema.safeParse({
|
|
title: formData.get("title"),
|
|
description: formData.get("description"),
|
|
categoryId: formData.get("categoryId"),
|
|
city: formData.get("city"),
|
|
region: formData.get("region") || undefined,
|
|
budgetMin: formData.get("budgetMin") || undefined,
|
|
budgetMax: formData.get("budgetMax") || undefined,
|
|
});
|
|
|
|
if (!validatedFields.success) {
|
|
return { errors: validatedFields.error.flatten().fieldErrors };
|
|
}
|
|
|
|
const { title, description, categoryId, city, region, budgetMin, budgetMax } =
|
|
validatedFields.data;
|
|
|
|
const parsedBudgetMin = budgetMin ? Number(budgetMin) : null;
|
|
const parsedBudgetMax = budgetMax ? Number(budgetMax) : null;
|
|
|
|
if (
|
|
(budgetMin && Number.isNaN(parsedBudgetMin)) ||
|
|
(budgetMax && Number.isNaN(parsedBudgetMax))
|
|
) {
|
|
return { message: "Budget invalide." };
|
|
}
|
|
if (
|
|
parsedBudgetMin !== null &&
|
|
parsedBudgetMax !== null &&
|
|
parsedBudgetMin > parsedBudgetMax
|
|
) {
|
|
return { message: "Le budget minimum doit être inférieur au maximum." };
|
|
}
|
|
|
|
const images = formData
|
|
.getAll("images")
|
|
.filter((entry): entry is File => entry instanceof File && entry.size > 0);
|
|
|
|
if (images.length > MAX_IMAGES) {
|
|
return { message: `${MAX_IMAGES} images maximum.` };
|
|
}
|
|
if (images.some((file) => file.size > MAX_IMAGE_SIZE)) {
|
|
return { message: "Chaque image doit faire moins de 5 Mo." };
|
|
}
|
|
|
|
const supabase = await createClient();
|
|
|
|
const { data: need, error } = await supabase
|
|
.from("needs")
|
|
.insert({
|
|
author_id: user.id,
|
|
category_id: categoryId,
|
|
title,
|
|
description,
|
|
city,
|
|
region: region || null,
|
|
budget_min: parsedBudgetMin,
|
|
budget_max: parsedBudgetMax,
|
|
})
|
|
.select("id")
|
|
.single();
|
|
|
|
if (error || !need) {
|
|
return { message: "Une erreur est survenue lors de la création du besoin." };
|
|
}
|
|
|
|
for (const [index, file] of images.entries()) {
|
|
const path = `${user.id}/${need.id}/${index}-${file.name}`;
|
|
const { error: uploadError } = await supabase.storage
|
|
.from("need-images")
|
|
.upload(path, file);
|
|
|
|
if (!uploadError) {
|
|
await supabase
|
|
.from("need_images")
|
|
.insert({ need_id: need.id, storage_path: path, position: index });
|
|
}
|
|
}
|
|
|
|
redirect(`/needs/${need.id}`);
|
|
}
|