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

156 lines
4.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Link from "next/link";
import { notFound } from "next/navigation";
import Image from "next/image";
import { createClient } from "@/lib/supabase/server";
import { getCurrentUser } from "@/lib/dal";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { OfferForm } from "@/components/offers/offer-form";
import { OfferList } from "@/components/offers/offer-list";
export default async function NeedDetailPage({
params,
}: {
params: Promise<{ id: string }>;
}) {
const { id } = await params;
const supabase = await createClient();
const user = await getCurrentUser();
const { data: need } = await supabase
.from("needs")
.select(
"id, title, description, city, region, budget_min, budget_max, status, author_id, categories(name), need_images(storage_path, position)"
)
.eq("id", id)
.single();
if (!need) {
notFound();
}
const images = [...(need.need_images ?? [])].sort(
(a, b) => a.position - b.position
);
const isOwner = user?.id === need.author_id;
const category = Array.isArray(need.categories)
? need.categories[0]
: need.categories;
const { data: offers } = await supabase
.from("offers")
.select(
"id, title, description, price, status, merchant_id, profiles(full_name), offer_images(storage_path, position)"
)
.eq("need_id", id)
.order("created_at", { ascending: true });
const offerList = offers ?? [];
const myOffer = user ? offerList.find((o) => o.merchant_id === user.id) : undefined;
function getOfferImageUrl(path: string) {
return supabase.storage.from("offer-images").getPublicUrl(path).data
.publicUrl;
}
return (
<div className="mx-auto max-w-2xl px-4 py-8 sm:px-6 sm:py-10">
<div className="flex items-center gap-2">
{category?.name && <Badge variant="secondary">{category.name}</Badge>}
<Badge
variant={need.status === "open" ? "default" : "outline"}
>
{need.status}
</Badge>
</div>
<h1 className="mt-3 text-2xl font-semibold">{need.title}</h1>
<p className="mt-1 text-muted-foreground">
{need.city}
{need.region ? `, ${need.region}` : ""}
</p>
{(need.budget_min || need.budget_max) && (
<p className="mt-1 text-sm">
Budget : {need.budget_min ?? "?"} {need.budget_max ?? "?"}
</p>
)}
{images.length > 0 && (
<div className="mt-6 flex flex-wrap gap-3">
{images.map((img) => {
const { data } = supabase.storage
.from("need-images")
.getPublicUrl(img.storage_path);
return (
<div
key={img.storage_path}
className="relative h-32 w-32 overflow-hidden rounded-md border"
>
<Image
src={data.publicUrl}
alt={need.title}
fill
className="object-cover"
/>
</div>
);
})}
</div>
)}
<p className="mt-6 whitespace-pre-wrap">{need.description}</p>
<div className="mt-10 border-t pt-6">
{isOwner ? (
<>
<h2 className="text-lg font-semibold">Offres reçues</h2>
<OfferList
offers={offerList}
needId={need.id}
needStatus={need.status}
isOwner
getImageUrl={getOfferImageUrl}
/>
</>
) : !user ? (
<p className="text-sm text-muted-foreground">
<Link href="/auth/login" className="underline">
Connecte-toi
</Link>{" "}
pour proposer une offre sur ce besoin.
</p>
) : myOffer ? (
<>
<h2 className="text-lg font-semibold">Ton offre</h2>
<OfferList
offers={[myOffer]}
needId={need.id}
needStatus={need.status}
isOwner={false}
getImageUrl={getOfferImageUrl}
/>
</>
) : need.status === "open" ? (
<>
<h2 className="text-lg font-semibold">Proposer une offre</h2>
<div className="mt-4">
<OfferForm needId={need.id} />
</div>
</>
) : (
<p className="text-sm text-muted-foreground">
Ce besoin n&apos;accepte plus d&apos;offres.
</p>
)}
</div>
{isOwner && (
<p className="mt-4 text-xs text-muted-foreground">
<Button asChild variant="link" className="h-auto p-0 text-xs">
<Link href="/messages">Voir mes conversations</Link>
</Button>
</p>
)}
</div>
);
}