-
-
- {nightlyPrice.toFixed(0)} € × {nights} nuit{nights > 1 ? "s" : ""}
-
-
{(nightlyPrice * nights).toFixed(2)} €
+ {datesSelected ? (
+
+
+
+ {nightlyPrice.toFixed(0)} € × {nights} nuit{nights > 1 ? "s" : ""}
+
+ {(nightlyPrice * nights).toFixed(2)} €
+
+
+ Total
+ {total.toFixed(2)} €
+
-
- Total
- {total.toFixed(2)} €
-
-
+ ) : null}
- {!nightsOk && nights > 0 ? (
+ {datesSelected && !nightsOk ? (
Séjour entre {minN} et {maxN} nuits requis.
) : null}
- {hasConflict ? (
-
- Cette plage chevauche {conflictDates.length} jour{conflictDates.length > 1 ? "s" : ""} déjà
- pris ou bloqué{conflictDates.length > 1 ? "s" : ""} (
- {conflictDates.slice(0, 3).join(", ")}
- {conflictDates.length > 3 ? "…" : ""}). Changez les dates.
-
- ) : null}
-
- {upcomingBlocked.length > 0 && !hasConflict ? (
-
- Voir les prochaines dates indisponibles
-
- {upcomingBlocked.map((d) => (
-
- {d}
-
- ))}
- {blockedDates.size > upcomingBlocked.length ? (
- + {blockedDates.size - upcomingBlocked.length} autres
- ) : null}
-
-
- ) : null}
-
{error ? (
{error}
) : null}
diff --git a/src/app/carbets/_components/carbet-map-inner.tsx b/src/app/carbets/_components/carbet-map-inner.tsx
new file mode 100644
index 0000000..63d1ab6
--- /dev/null
+++ b/src/app/carbets/_components/carbet-map-inner.tsx
@@ -0,0 +1,74 @@
+"use client";
+
+import { MapContainer, TileLayer, Marker, Popup } from "react-leaflet";
+import L from "leaflet";
+
+import "leaflet/dist/leaflet.css";
+
+// Fix icône Leaflet (les paths par défaut pointent vers un CDN qui n'existe plus).
+// On utilise un SVG inline en data URL.
+const ICON = L.divIcon({
+ className: "karbe-leaflet-marker",
+ html: `
+
+ `,
+ iconSize: [32, 40],
+ iconAnchor: [16, 40],
+ popupAnchor: [0, -36],
+});
+
+type Props = {
+ latitude: number;
+ longitude: number;
+ title: string;
+ river: string;
+ embarkPoint: string;
+};
+
+export function CarbetMapInner({ latitude, longitude, title, river, embarkPoint }: Props) {
+ const position: [number, number] = [latitude, longitude];
+
+ return (
+
+ );
+}
diff --git a/src/app/carbets/_components/carbet-map.tsx b/src/app/carbets/_components/carbet-map.tsx
new file mode 100644
index 0000000..31b9718
--- /dev/null
+++ b/src/app/carbets/_components/carbet-map.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+/**
+ * Carte interactive sur la fiche carbet — Leaflet + OpenStreetMap.
+ *
+ * Chargée dynamiquement (ssr:false) car Leaflet manipule window.
+ */
+
+import dynamic from "next/dynamic";
+
+const CarbetMapInner = dynamic(
+ () => import("./carbet-map-inner").then((m) => m.CarbetMapInner),
+ {
+ ssr: false,
+ loading: () => (
+
+ ),
+ },
+);
+
+type Props = {
+ latitude: number;
+ longitude: number;
+ title: string;
+ river: string;
+ embarkPoint: string;
+};
+
+export function CarbetMap(props: Props) {
+ return
;
+}
diff --git a/src/app/carbets/_components/mini-calendar.tsx b/src/app/carbets/_components/mini-calendar.tsx
new file mode 100644
index 0000000..bdcbb0d
--- /dev/null
+++ b/src/app/carbets/_components/mini-calendar.tsx
@@ -0,0 +1,186 @@
+"use client";
+
+import { useMemo, useState } from "react";
+
+type Props = {
+ startDate: string | null;
+ endDate: string | null;
+ blockedDates: Set
;
+ onChange: (start: string | null, end: string | null) => void;
+};
+
+const MONTH_LABEL = [
+ "Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
+ "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre",
+];
+const DOW_LABEL = ["L", "M", "M", "J", "V", "S", "D"];
+
+function isoDay(d: Date): string {
+ return d.toISOString().slice(0, 10);
+}
+
+function startOfMonth(d: Date): Date {
+ return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1));
+}
+
+function addMonths(d: Date, n: number): Date {
+ return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + n, 1));
+}
+
+/** Génère la grille du mois : 6 semaines × 7 jours, en commençant un lundi. */
+function monthGrid(monthStart: Date): (Date | null)[] {
+ const year = monthStart.getUTCFullYear();
+ const month = monthStart.getUTCMonth();
+ // Premier jour du mois — décale pour que la semaine commence un lundi (0=L, 6=D)
+ const firstDay = new Date(Date.UTC(year, month, 1));
+ const firstDow = (firstDay.getUTCDay() + 6) % 7;
+ const lastDay = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
+ const cells: (Date | null)[] = [];
+ for (let i = 0; i < firstDow; i++) cells.push(null);
+ for (let d = 1; d <= lastDay; d++) {
+ cells.push(new Date(Date.UTC(year, month, d)));
+ }
+ while (cells.length % 7 !== 0) cells.push(null);
+ // Toujours 6 lignes pour éviter le saut de hauteur
+ while (cells.length < 42) cells.push(null);
+ return cells;
+}
+
+export function MiniCalendar({ startDate, endDate, blockedDates, onChange }: Props) {
+ const today = useMemo(() => {
+ const d = new Date();
+ return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
+ }, []);
+
+ const [viewMonth, setViewMonth] = useState(() => {
+ const ref = startDate ? new Date(startDate + "T00:00:00Z") : today;
+ return startOfMonth(ref);
+ });
+
+ const cells = useMemo(() => monthGrid(viewMonth), [viewMonth]);
+
+ const startISO = startDate;
+ const endISO = endDate;
+
+ function onClick(day: Date) {
+ const iso = isoDay(day);
+ if (day.getTime() < today.getTime()) return;
+ if (blockedDates.has(iso)) return;
+
+ // Aucune sélection ou les deux déjà posées → reset + nouvelle start
+ if (!startISO || (startISO && endISO)) {
+ onChange(iso, null);
+ return;
+ }
+ // Une seule (start) déjà sélectionnée
+ if (iso === startISO) {
+ onChange(null, null);
+ return;
+ }
+ if (iso < startISO) {
+ onChange(iso, null);
+ return;
+ }
+ // Vérifie qu'aucun jour intermédiaire n'est bloqué
+ const startMs = new Date(startISO + "T00:00:00Z").getTime();
+ const endMs = day.getTime();
+ for (let t = startMs; t < endMs; t += 86_400_000) {
+ const d = new Date(t).toISOString().slice(0, 10);
+ if (blockedDates.has(d)) {
+ // Tombe sur un jour bloqué → on resélectionne start
+ onChange(iso, null);
+ return;
+ }
+ }
+ onChange(startISO, iso);
+ }
+
+ const canGoBack = viewMonth > startOfMonth(today);
+
+ return (
+
+
+
+
+ {MONTH_LABEL[viewMonth.getUTCMonth()]} {viewMonth.getUTCFullYear()}
+
+
+
+
+
+ {DOW_LABEL.map((d, i) => (
+
+ {d}
+
+ ))}
+
+
+
+ {cells.map((cell, i) => {
+ if (!cell) return
;
+ const iso = isoDay(cell);
+ const isPast = cell.getTime() < today.getTime();
+ const isBlocked = blockedDates.has(iso);
+ const isStart = iso === startISO;
+ const isEnd = iso === endISO;
+ const inRange = startISO && endISO && iso > startISO && iso < endISO;
+ const isToday = iso === isoDay(today);
+ const disabled = isPast || isBlocked;
+
+ let cls =
+ "relative h-7 rounded text-xs flex items-center justify-center transition";
+ if (disabled) {
+ cls += " text-zinc-300 cursor-not-allowed";
+ if (isBlocked && !isPast) cls += " line-through";
+ } else if (isStart || isEnd) {
+ cls += " bg-emerald-600 text-white font-semibold cursor-pointer";
+ } else if (inRange) {
+ cls += " bg-emerald-100 text-emerald-900 cursor-pointer";
+ } else {
+ cls += " text-zinc-800 hover:bg-zinc-100 cursor-pointer";
+ if (isToday) cls += " ring-1 ring-zinc-400";
+ }
+
+ return (
+
+ );
+ })}
+
+
+
+ {!startISO
+ ? "Choisissez votre date d'arrivée."
+ : !endISO
+ ? "Choisissez votre date de départ."
+ : ""}
+
+
+ );
+}