Files
softgrey af81124d48 Initial commit: WaMarket MVP
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.
2026-07-30 14:33:17 +00:00

116 lines
3.0 KiB
TypeScript

"use server";
import { redirect } from "next/navigation";
import { revalidatePath } from "next/cache";
import { createClient } from "@/lib/supabase/server";
import { verifySession } from "@/lib/dal";
import { OfferFormSchema, type OfferFormState } from "@/lib/validations/offers";
const MAX_IMAGES = 5;
const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5 Mo
export async function createOffer(
_state: OfferFormState,
formData: FormData
): Promise<OfferFormState> {
const user = await verifySession();
const needId = formData.get("needId");
if (typeof needId !== "string" || !needId) {
return { message: "Besoin introuvable." };
}
const validatedFields = OfferFormSchema.safeParse({
title: formData.get("title"),
description: formData.get("description"),
price: formData.get("price"),
});
if (!validatedFields.success) {
return { errors: validatedFields.error.flatten().fieldErrors };
}
const { title, description, price } = validatedFields.data;
const parsedPrice = Number(price);
if (Number.isNaN(parsedPrice) || parsedPrice <= 0) {
return { message: "Prix invalide." };
}
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: offer, error } = await supabase
.from("offers")
.insert({
need_id: needId,
merchant_id: user.id,
title,
description,
price: parsedPrice,
})
.select("id")
.single();
if (error || !offer) {
return {
message:
"Impossible de créer l'offre (le besoin n'est peut-être plus ouvert, ou tu as déjà proposé une offre).",
};
}
for (const [index, file] of images.entries()) {
const path = `${user.id}/${offer.id}/${index}-${file.name}`;
const { error: uploadError } = await supabase.storage
.from("offer-images")
.upload(path, file);
if (!uploadError) {
await supabase
.from("offer_images")
.insert({ offer_id: offer.id, storage_path: path, position: index });
}
}
revalidatePath(`/needs/${needId}`);
redirect(`/needs/${needId}`);
}
export async function withdrawOffer(offerId: string, needId: string) {
await verifySession();
const supabase = await createClient();
await supabase
.from("offers")
.update({ status: "withdrawn" })
.eq("id", offerId)
.eq("status", "pending");
revalidatePath(`/needs/${needId}`);
}
export async function acceptOffer(offerId: string, needId: string) {
await verifySession();
const supabase = await createClient();
const { data: conversationId, error } = await supabase.rpc("accept_offer", {
p_offer_id: offerId,
});
if (error || !conversationId) {
revalidatePath(`/needs/${needId}`);
return;
}
redirect(`/messages/${conversationId}`);
}