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.
65 lines
2.1 KiB
TypeScript
65 lines
2.1 KiB
TypeScript
"use client";
|
|
|
|
import { useActionState } from "react";
|
|
import { createOffer } from "@/lib/actions/offers";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Label } from "@/components/ui/label";
|
|
import { ImageUploader } from "@/components/shared/image-uploader";
|
|
|
|
export function OfferForm({ needId }: { needId: string }) {
|
|
const [state, action, pending] = useActionState(createOffer, undefined);
|
|
|
|
return (
|
|
<form action={action} className="flex flex-col gap-4">
|
|
<input type="hidden" name="needId" value={needId} />
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<Label htmlFor="offer-title">Titre de l'offre</Label>
|
|
<Input
|
|
id="offer-title"
|
|
name="title"
|
|
placeholder="Ex: Intervention sous 48h avec garantie"
|
|
/>
|
|
{state?.errors?.title && (
|
|
<p className="text-sm text-destructive">{state.errors.title[0]}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<Label htmlFor="offer-description">Description</Label>
|
|
<Textarea
|
|
id="offer-description"
|
|
name="description"
|
|
rows={4}
|
|
placeholder="Détaille ce que tu proposes, tes conditions, ton expérience..."
|
|
/>
|
|
{state?.errors?.description && (
|
|
<p className="text-sm text-destructive">
|
|
{state.errors.description[0]}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-2">
|
|
<Label htmlFor="offer-price">Prix (€)</Label>
|
|
<Input id="offer-price" name="price" type="number" min="0" step="0.01" />
|
|
{state?.errors?.price && (
|
|
<p className="text-sm text-destructive">{state.errors.price[0]}</p>
|
|
)}
|
|
</div>
|
|
|
|
<ImageUploader name="images" maxFiles={5} />
|
|
|
|
{state?.message && (
|
|
<p className="text-sm text-destructive">{state.message}</p>
|
|
)}
|
|
|
|
<Button type="submit" disabled={pending} className="mt-2 w-fit">
|
|
{pending ? "Envoi..." : "Envoyer l'offre"}
|
|
</Button>
|
|
</form>
|
|
);
|
|
}
|