diff --git a/src/app/admin/carbets/[id]/_components/MediaManager.tsx b/src/app/admin/carbets/[id]/_components/MediaManager.tsx new file mode 100644 index 0000000..47947da --- /dev/null +++ b/src/app/admin/carbets/[id]/_components/MediaManager.tsx @@ -0,0 +1,142 @@ +"use client"; + +import { useState, useTransition } from "react"; +import Image from "next/image"; +import { addMediaAction, removeMediaAction, reorderMediaAction } from "../../actions"; +import { FormField, inputCls, selectCls } from "@/components/admin/FormField"; + +type MediaItem = { + id: string; + type: "PHOTO" | "VIDEO"; + s3Key: string; + s3Url: string; + sortOrder: number; +}; + +export function MediaManager({ carbetId, media: initial }: { carbetId: string; media: MediaItem[] }) { + const [media, setMedia] = useState(initial); + const [pending, startTransition] = useTransition(); + const [error, setError] = useState(null); + + async function refresh() { + const r = await fetch(`/api/admin/carbets/${carbetId}/media`); + if (r.ok) setMedia(await r.json()); + } + + function addByUrl(fd: FormData) { + setError(null); + startTransition(async () => { + const res = await addMediaAction(carbetId, fd); + if (res?.ok === false) { + setError(res.error); + } else { + await refresh(); + } + }); + } + + function remove(mediaId: string) { + startTransition(async () => { + await removeMediaAction(carbetId, mediaId); + await refresh(); + }); + } + + function reorder(mediaId: string, dir: "up" | "down") { + startTransition(async () => { + await reorderMediaAction(carbetId, mediaId, dir); + await refresh(); + }); + } + + return ( +
+

Médias ({media.length})

+ + {media.length === 0 ? ( +

+ Aucun média. Ajoute une URL ci-dessous (MinIO, CDN externe, …). +

+ ) : ( + + )} + +
+

Ajouter un média par URL

+
+ + + + + + +
+ + {error ?
{error}
: null} +
+ +
+
+
+ ); +} diff --git a/src/app/admin/carbets/[id]/_components/StatusActions.tsx b/src/app/admin/carbets/[id]/_components/StatusActions.tsx new file mode 100644 index 0000000..7d585ef --- /dev/null +++ b/src/app/admin/carbets/[id]/_components/StatusActions.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { useRouter } from "next/navigation"; +import { CarbetStatus } from "@/generated/prisma/enums"; +import { deleteCarbetAction, updateCarbetStatusAction } from "../../actions"; + +type Status = (typeof CarbetStatus)[keyof typeof CarbetStatus]; + +export function StatusActions({ id, current }: { id: string; current: Status }) { + const router = useRouter(); + const [pending, startTransition] = useTransition(); + const [confirmArchive, setConfirmArchive] = useState(false); + + function setStatus(next: Status) { + startTransition(async () => { + await updateCarbetStatusAction(id, next); + router.refresh(); + }); + } + + function archive() { + startTransition(async () => { + await deleteCarbetAction(id); + }); + } + + return ( +
+ {current === CarbetStatus.DRAFT ? ( + + ) : null} + {current === CarbetStatus.PUBLISHED ? ( + + ) : null} + {current !== CarbetStatus.ARCHIVED ? ( + confirmArchive ? ( +
+ Sûr ? + + +
+ ) : ( + + ) + ) : ( + + )} +
+ ); +} diff --git a/src/app/admin/carbets/[id]/page.tsx b/src/app/admin/carbets/[id]/page.tsx new file mode 100644 index 0000000..5e8f635 --- /dev/null +++ b/src/app/admin/carbets/[id]/page.tsx @@ -0,0 +1,103 @@ +import { notFound } from "next/navigation"; +import Link from "next/link"; +import { + getCarbetForEdit, + listOwners, + listPirogueProviders, +} from "@/lib/admin/carbets"; +import { CarbetForm } from "../_components/CarbetForm"; +import { StatusBadge } from "@/components/admin/StatusBadge"; +import { MediaManager } from "./_components/MediaManager"; +import { StatusActions } from "./_components/StatusActions"; +import { updateCarbetAction } from "../actions"; + +export const dynamic = "force-dynamic"; + +type PageProps = { params: Promise<{ id: string }> }; + +export default async function EditCarbetPage({ params }: PageProps) { + const { id } = await params; + const [carbet, owners, providers] = await Promise.all([ + getCarbetForEdit(id), + listOwners(), + listPirogueProviders(), + ]); + if (!carbet) notFound(); + + const updateThis = async (fd: FormData) => { + "use server"; + return await updateCarbetAction(id, fd); + }; + + return ( +
+
+
+ + ← Tous les carbets + +

+ {carbet.title} + +

+

+ /{carbet.slug} · {carbet._count.bookings} résa + {carbet._count.bookings > 1 ? "s" : ""} · {carbet._count.reviews} avis · + mis à jour {new Intl.DateTimeFormat("fr-FR", { day: "2-digit", month: "long", year: "numeric" }).format(carbet.updatedAt)} +

+
+
+ + {carbet.status === "PUBLISHED" ? ( + + ↗ Voir la fiche publique + + ) : null} +
+
+ + ({ + id: m.id, + type: m.type, + s3Key: m.s3Key, + s3Url: m.s3Url, + sortOrder: m.sortOrder, + }))} + /> + + +
+ ); +} diff --git a/src/app/admin/carbets/_components/CarbetForm.tsx b/src/app/admin/carbets/_components/CarbetForm.tsx new file mode 100644 index 0000000..11d8460 --- /dev/null +++ b/src/app/admin/carbets/_components/CarbetForm.tsx @@ -0,0 +1,269 @@ +"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/carbets"; + +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(null); + const [success, setSuccess] = useState(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 ( +
+
+ {/* Identité */} +
+

Identité

+
+ + + + + + + + + + +