// ----------------------------------------------------------------------------- // Lou-Ka — Location court terme // pages/CourtTermeFiche.tsx : fiche d'un hébergement court terme. // Ordre DOM = ordre visuel, identique mobile ET desktop (standard Groupe Ka // « Ordre des sections — pages détail ») : galerie → prix + titre + chips + // CTA → description → commodités → détails pratiques → carte. // ----------------------------------------------------------------------------- import { lazy, Suspense, useEffect, useRef, useState } from "react"; import { Link, useParams } from "react-router-dom"; import { CtContext, CtListing, ctSourceCategory, ctSourceName, fetchCtContext, fetchCtListing, fetchCtSources, fmtNight, registerCtSourceNames, } from "../ctapi"; import SmartImg from "../components/SmartImg"; import CtListingCard from "../components/CtListingCard"; import CtPriceAnalysis, { ctDealBadge } from "../components/CtPriceAnalysis"; import { IcoAlert } from "../components/Icons"; const CtFicheMap = lazy(() => import("../components/CtFicheMap")); const NBSP = " "; const PETS_LABEL: Record = { oui: "Animaux acceptés", non: "Animaux refusés", conditions: "Animaux sous conditions", }; const DETAIL_LABELS: Record = { spa: "Spa", pool: "Piscine", waterfront: "Bord de l'eau", sauna: "Sauna", wifi: "Wi-Fi", fireplace: "Foyer", ev_charger: "Borne de recharge", }; function Galerie({ images, titre, typeLabel }: { images: string[]; titre: string; typeLabel?: string }) { const [idx, setIdx] = useState(0); const track = useRef(null); const onScroll = () => { const el = track.current; if (el) setIdx(Math.round(el.scrollLeft / el.clientWidth)); }; const goto = (i: number) => track.current?.scrollTo({ left: i * track.current.clientWidth, behavior: "smooth" }); if (images.length === 0) return (
); return ( <>
{images.map((u, i) => ( ))}
{idx + 1}/{images.length} {idx > 0 && ( )} {idx < images.length - 1 && ( )}
{images.length > 1 && (
{images.map((u, i) => ( ))}
)} ); } export default function CourtTermeFichePage() { const { uid } = useParams<{ uid: string }>(); const [l, setL] = useState(null); const [ctx, setCtx] = useState(null); const [error, setError] = useState(null); useEffect(() => { fetchCtSources().then((r) => registerCtSourceNames(r.sources)).catch(() => {}); if (!uid) return; setCtx(null); fetchCtListing(uid).then(setL).catch((e) => setError(String(e))); fetchCtContext(uid).then(setCtx).catch(() => setCtx(null)); window.scrollTo(0, 0); }, [uid]); if (error) return (

Hébergement introuvable

{error}

Retour au court terme
); if (!l) return (
); const chips: string[] = []; if (l.property_type) chips.push(l.property_type); if (l.capacity) chips.push(`${l.capacity} personnes`); if (l.bedrooms) chips.push(`${l.bedrooms} chambre${l.bedrooms > 1 ? "s" : ""}`); if (l.beds) chips.push(`${l.beds} lit${l.beds > 1 ? "s" : ""}`); if (l.bathrooms) chips.push(`${l.bathrooms} salle${l.bathrooms > 1 ? "s" : ""} de bain`); if (l.pets) chips.push(PETS_LABEL[l.pets] ?? l.pets); if (l.citq) chips.push(`CITQ ${l.citq}`); const flags = Object.entries(DETAIL_LABELS) .filter(([k]) => l.details?.[k] === true || l.details?.[k] === 1) .map(([, label]) => label); const autres = l.amenities.filter( (a) => !flags.some((b) => b.toLowerCase() === a.toLowerCase())); const ctaLabel = ctSourceCategory(l.source) === "hotels" ? "Réserver en direct à l'hôtel" : `Réserver chez ${ctSourceName(l.source)}`; const synced = l.last_seen ? new Date(l.last_seen * 1000).toLocaleDateString("fr-CA", { day: "numeric", month: "long", year: "numeric" }) : null; return (
{fmtNight(l.price_night, l.price_label)} {l.price_night != null && /{NBSP}nuit}
{(() => { const badge = ctDealBadge(ctx?.price ?? null); return badge && (
{badge.txt}
); })()} {l.rating != null && (
★ {l.rating.toLocaleString("fr-CA", { maximumFractionDigits: 1 })} / 5 {l.reviews ? ` · ${l.reviews} avis` : ""}
)}

{l.title}

{[l.address, l.city, l.region].filter(Boolean).join(" · ")}
{chips.map((c) => {c})}
{ctaLabel} ↗

Description

{l.description ?

{l.description}

:

La source ne fournit pas de description pour cet hébergement.

}

Commodités

{flags.map((b) => ( ✓ {b} ))} {autres.map((a) => ( {a} ))}
{flags.length === 0 && autres.length === 0 && (

La source ne précise pas les commodités.

)}

Détails pratiques

Plateforme
{ctSourceName(l.source)}
{l.price_label && (
Prix affiché
{l.price_label}
)} {l.citq && (
Enregistrement CITQ
{l.citq}
)} {synced && (
Synchronisé
{synced}
)}
{!l.citq && (

Au Québec, tout séjour de 31 nuits ou moins doit être offert par un établissement détenant un numéro d'enregistrement CITQ. Vérifiez le numéro affiché sur l'annonce originale avant de réserver.

)}
{l.lat != null && l.lng != null && (

Emplacement

}>

Position approximative selon les coordonnées fournies par la source.

)}
{ctx && ctx.similar.length > 0 && (

{l.lat != null && ctx.similar[0]?.distance_km != null ? "Hébergements similaires à proximité" : `Hébergements similaires — ${l.region || "même région"}`}

{ctx.similar.map((s) => )}
)}
{synced && <>Dernière synchronisation : {synced}. } Les prix et disponibilités sont ceux affichés par la source — chaque fiche renvoie à l'annonce originale pour réserver.
{fmtNight(l.price_night, l.price_label)} {l.price_night != null && /nuit} {ctaLabel} ↗
); }