karbe/src/app/admin/carbets/_components/CarbetForm.tsx
Claude Integration fc01144e0e chore(admin): split options enum dans fichier neutre
Le client component CarbetForm importait des options depuis lib/admin/carbets
qui contient "server-only" → erreur build turbopack. Sortie des options dans
src/lib/admin/carbet-options.ts sans server-only.
2026-05-31 21:06:47 +00:00

269 lines
10 KiB
TypeScript

"use client";
import { useState, useTransition } from "react";
import { FormField, inputCls, selectCls, textareaCls } from "@/components/admin/FormField";
import {
ACCESS_TYPE_OPTIONS,
STATUS_OPTIONS,
TRANSPORT_MODE_OPTIONS,
} from "@/lib/admin/carbet-options";
export type CarbetFormInitial = {
ownerId?: string;
title?: string;
slug?: string;
description?: string;
river?: string;
embarkPoint?: string;
latitude?: number | string;
longitude?: number | string;
capacity?: number;
accessType?: string;
roadAccessNote?: string | null;
pirogueDurationMin?: number | null;
minStayNights?: number | null;
maxStayNights?: number | null;
minCapacity?: number | null;
transportMode?: string | null;
pirogueProviderId?: string | null;
status?: string;
};
type Props = {
initial?: CarbetFormInitial;
owners: { id: string; firstName: string; lastName: string; email: string }[];
providers: { id: string; name: string; rivers: string[] }[];
action: (fd: FormData) => Promise<{ ok: false; error: string } | { ok: true } | undefined>;
submitLabel?: string;
};
export function CarbetForm({ initial = {}, owners, providers, action, submitLabel = "Enregistrer" }: Props) {
const [pending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
function onSubmit(formData: FormData) {
setError(null);
setSuccess(null);
startTransition(async () => {
const res = await action(formData);
if (res && res.ok === false) {
setError(res.error);
} else if (res && res.ok === true) {
setSuccess("Carbet enregistré.");
}
});
}
return (
<form action={onSubmit} className="space-y-6">
<fieldset disabled={pending} className="space-y-6">
{/* Identité */}
<section className="rounded-lg border border-zinc-200 bg-white p-5 shadow-sm">
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-zinc-500">Identité</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<FormField label="Titre" required>
<input name="title" defaultValue={initial.title ?? ""} className={inputCls} required maxLength={200} />
</FormField>
<FormField label="Slug" required hint="URL publique : /carbets/<slug>">
<input
name="slug"
defaultValue={initial.slug ?? ""}
className={inputCls}
required
pattern="^[a-z0-9](?:[a-z0-9-]{0,80}[a-z0-9])?$"
placeholder="ex. karbe-awara-maroni"
/>
</FormField>
<FormField label="Propriétaire" required className="sm:col-span-2">
<select name="ownerId" defaultValue={initial.ownerId ?? ""} className={selectCls} required>
<option value="" disabled> sélectionner un propriétaire </option>
{owners.map((o) => (
<option key={o.id} value={o.id}>
{o.firstName} {o.lastName} ({o.email})
</option>
))}
</select>
</FormField>
<FormField label="Description" required className="sm:col-span-2" hint="Markdown léger autorisé.">
<textarea
name="description"
rows={6}
defaultValue={initial.description ?? ""}
className={textareaCls}
required
maxLength={20000}
/>
</FormField>
</div>
</section>
{/* Localisation */}
<section className="rounded-lg border border-zinc-200 bg-white p-5 shadow-sm">
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-zinc-500">Localisation</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<FormField label="Fleuve" required>
<input name="river" defaultValue={initial.river ?? ""} className={inputCls} required maxLength={100} placeholder="Maroni" />
</FormField>
<FormField label="Point d'embarquement" required>
<input
name="embarkPoint"
defaultValue={initial.embarkPoint ?? ""}
className={inputCls}
required
maxLength={200}
/>
</FormField>
<FormField label="Latitude" required hint="Décimal (-90 à 90)">
<input
name="latitude"
type="number"
step="0.000001"
defaultValue={initial.latitude?.toString() ?? ""}
className={inputCls}
required
/>
</FormField>
<FormField label="Longitude" required hint="Décimal (-180 à 180)">
<input
name="longitude"
type="number"
step="0.000001"
defaultValue={initial.longitude?.toString() ?? ""}
className={inputCls}
required
/>
</FormField>
</div>
</section>
{/* Accès */}
<section className="rounded-lg border border-zinc-200 bg-white p-5 shadow-sm">
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-zinc-500">Accès & transport</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
<FormField label="Type d'accès" required>
<select name="accessType" defaultValue={initial.accessType ?? "ROAD_AND_RIVER"} className={selectCls} required>
{ACCESS_TYPE_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</FormField>
<FormField label="Durée pirogue (min)" hint="Optionnel — vide si accès route uniquement">
<input
name="pirogueDurationMin"
type="number"
min={0}
max={1440}
defaultValue={initial.pirogueDurationMin?.toString() ?? ""}
className={inputCls}
/>
</FormField>
<FormField label="Note d'accès route" className="sm:col-span-2" hint="GPS, type de piste, distance dernière ville…">
<textarea
name="roadAccessNote"
rows={2}
defaultValue={initial.roadAccessNote ?? ""}
className={textareaCls}
maxLength={1000}
/>
</FormField>
<FormField label="Mode de transport pirogue">
<select name="transportMode" defaultValue={initial.transportMode ?? ""} className={selectCls}>
<option value=""> non spécifié </option>
{TRANSPORT_MODE_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</FormField>
<FormField label="Prestataire pirogue partenaire">
<select name="pirogueProviderId" defaultValue={initial.pirogueProviderId ?? ""} className={selectCls}>
<option value=""> aucun </option>
{providers.map((p) => (
<option key={p.id} value={p.id}>
{p.name} ({p.rivers.join(", ")})
</option>
))}
</select>
</FormField>
</div>
</section>
{/* Séjour */}
<section className="rounded-lg border border-zinc-200 bg-white p-5 shadow-sm">
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-zinc-500">Séjour</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-4">
<FormField label="Capacité" required hint="Voyageurs max">
<input
name="capacity"
type="number"
min={1}
max={100}
defaultValue={initial.capacity?.toString() ?? ""}
className={inputCls}
required
/>
</FormField>
<FormField label="Capacité min recommandée" hint="Facultatif">
<input
name="minCapacity"
type="number"
min={1}
max={100}
defaultValue={initial.minCapacity?.toString() ?? ""}
className={inputCls}
/>
</FormField>
<FormField label="Nuits min" hint="Facultatif">
<input
name="minStayNights"
type="number"
min={1}
max={365}
defaultValue={initial.minStayNights?.toString() ?? ""}
className={inputCls}
/>
</FormField>
<FormField label="Nuits max" hint="Facultatif">
<input
name="maxStayNights"
type="number"
min={1}
max={365}
defaultValue={initial.maxStayNights?.toString() ?? ""}
className={inputCls}
/>
</FormField>
</div>
</section>
{/* Publication */}
<section className="rounded-lg border border-zinc-200 bg-white p-5 shadow-sm">
<h2 className="mb-4 text-sm font-semibold uppercase tracking-wider text-zinc-500">Publication</h2>
<FormField label="Statut" hint="Brouillon n'apparaît pas sur le site public. Archivé reste en base mais non listé.">
<select name="status" defaultValue={initial.status ?? "DRAFT"} className={selectCls}>
{STATUS_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>{o.label}</option>
))}
</select>
</FormField>
</section>
{error ? (
<div className="rounded border border-rose-200 bg-rose-50 px-3 py-2 text-sm text-rose-700">{error}</div>
) : null}
{success ? (
<div className="rounded border border-emerald-200 bg-emerald-50 px-3 py-2 text-sm text-emerald-800">{success}</div>
) : null}
<div className="flex items-center justify-end gap-2">
<button
type="submit"
className="rounded-md bg-zinc-900 px-5 py-2 text-sm font-semibold text-white hover:bg-zinc-800 disabled:opacity-50"
>
{pending ? "Enregistrement…" : submitLabel}
</button>
</div>
</fieldset>
</form>
);
}