import { useState, useEffect, useMemo } from "react"; import { Plus, RefreshCw, Search, SlidersHorizontal, Home, ArrowUpDown, Loader2, MapPin, } from "lucide-react"; import AppartCard from "./components/AppartCard"; import AddModal from "./components/AddModal"; import { fetchAnnonces, addAnnonce, deleteAnnonce } from "./api"; const SORT_OPTIONS = [ { value: "prix_asc", label: "Prix croissant" }, { value: "prix_desc", label: "Prix decroissant" }, { value: "surface_desc", label: "Surface decroissante" }, { value: "note_desc", label: "Meilleure note" }, { value: "recent", label: "Plus recent" }, ]; const DATE_RANGE_OPTIONS = [ { value: "", label: "Date: Toutes" }, { value: "today", label: "Aujourd'hui" }, { value: "yesterday", label: "Hier" }, { value: "last7", label: "7 derniers jours" }, { value: "last30", label: "30 derniers jours" }, ]; const parseDate = (value) => { if (!value) return null; const toDate = (input) => { const date = new Date(input); return Number.isNaN(date.getTime()) ? null : date; }; let date = value instanceof Date ? value : null; if (!date && (typeof value === "number" || typeof value === "string")) { date = toDate(value); if (!date && typeof value === "string") { const trimmed = value.trim(); const dayMatch = trimmed.match(/^([0-9]{1,2})[\/\-]([0-9]{1,2})[\/\-]([0-9]{2,4})$/); if (dayMatch) { const [, day, month, year] = dayMatch; const normalizedYear = year.length === 2 ? `20${year}` : year; date = toDate(`${normalizedYear.padStart(4, "0")}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`); } else { const monthMatch = trimmed.match(/^([0-9]{1,2})[\/\-]([0-9]{2,4})$/); if (monthMatch) { const [, month, year] = monthMatch; const normalizedYear = year.length === 2 ? `20${year}` : year; date = toDate(`${normalizedYear.padStart(4, "0")}-${month.padStart(2, "0")}-01`); } } } } return date; }; const matchesDateRange = (annonce, range) => { if (!range) return true; const today = new Date(); const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); const startOfTomorrow = new Date(startOfToday); startOfTomorrow.setDate(startOfTomorrow.getDate() + 1); let start = null; let end = null; switch (range) { case "today": start = startOfToday; end = startOfTomorrow; break; case "yesterday": { const startOfYesterday = new Date(startOfToday); startOfYesterday.setDate(startOfYesterday.getDate() - 1); start = startOfYesterday; end = startOfToday; break; } case "last7": { start = new Date(startOfToday); start.setDate(start.getDate() - 6); end = startOfTomorrow; break; } case "last30": { start = new Date(startOfToday); start.setDate(start.getDate() - 29); end = startOfTomorrow; break; } default: return true; } const dates = [parseDate(annonce.DATE_DISPO), parseDate(annonce.createdAt)].filter(Boolean); if (!dates.length) return false; return dates.some((date) => date >= start && date < end); }; function App() { const [annonces, setAnnonces] = useState([]); const [loading, setLoading] = useState(true); const [showAdd, setShowAdd] = useState(false); const [search, setSearch] = useState(""); const [sort, setSort] = useState("recent"); const [filters, setFilters] = useState({ meuble: null, parking: null, balcon: null, maxPrix: "", minSurface: "", dateRange: "", }); const [showFilters, setShowFilters] = useState(false); const load = async () => { setLoading(true); try { const data = await fetchAnnonces(); setAnnonces(data); } catch (err) { console.error("Erreur chargement:", err); } setLoading(false); }; useEffect(() => { load(); }, []); const handleAdd = async (form) => { await addAnnonce(form); await load(); }; const handleDelete = async (id) => { await deleteAnnonce(id); await load(); }; const existingUrls = useMemo( () => annonces.map((a) => a.URL), [annonces] ); const filtered = useMemo(() => { let list = [...annonces]; if (search.trim()) { const q = search.toLowerCase(); list = list.filter( (a) => a.ADDRESS?.toLowerCase().includes(q) || a.URL?.toLowerCase().includes(q) || a.CHARGES?.toLowerCase().includes(q) ); } if (filters.meuble !== null) { list = list.filter((a) => a.MEUBLE === filters.meuble); } if (filters.parking !== null) { list = list.filter((a) => a.PARKING === filters.parking); } if (filters.balcon !== null) { list = list.filter((a) => a.BALCON === filters.balcon); } if (filters.maxPrix) { list = list.filter((a) => a.PRIX <= parseFloat(filters.maxPrix)); } if (filters.minSurface) { list = list.filter((a) => a.SURFACE >= parseInt(filters.minSurface)); } if (filters.dateRange) { list = list.filter((a) => matchesDateRange(a, filters.dateRange)); } switch (sort) { case "prix_asc": list.sort((a, b) => a.PRIX - b.PRIX); break; case "prix_desc": list.sort((a, b) => b.PRIX - a.PRIX); break; case "surface_desc": list.sort((a, b) => b.SURFACE - a.SURFACE); break; case "note_desc": list.sort( (a, b) => (b.NOTE_P || 0) + (b.NOTE_A || 0) - ((a.NOTE_P || 0) + (a.NOTE_A || 0)) ); break; case "recent": default: list.sort( (a, b) => new Date(b.createdAt) - new Date(a.createdAt) ); } return list; }, [annonces, search, sort, filters]); const activeFilterCount = [ filters.meuble !== null, filters.parking !== null, filters.balcon !== null, !!filters.maxPrix, !!filters.minSurface, !!filters.dateRange, ].filter(Boolean).length; const stats = useMemo(() => { if (!annonces.length) return null; const prices = annonces.map((a) => a.PRIX ?? 0).filter((p) => p > 0); if (!prices.length) return { count: annonces.length, avgPrix: 0, minPrix: 0, maxPrix: 0 }; return { count: annonces.length, avgPrix: (prices.reduce((s, p) => s + p, 0) / prices.length).toFixed(0), minPrix: Math.min(...prices).toFixed(0), maxPrix: Math.max(...prices).toFixed(0), }; }, [annonces]); return (
{/* Header */}

Appart Nantes

Paulin & Agathe {stats && ( -- {stats.count} annonce{stats.count > 1 ? "s" : ""} | moy.{" "} {stats.avgPrix} EUR | {stats.minPrix} - {stats.maxPrix} EUR )}

{/* Search + Sort + Filters toggle */}
setSearch(e.target.value)} placeholder="Rechercher par adresse..." className="w-full pl-10 pr-4 py-2.5 text-sm border border-sand-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-warm-300/50 focus:border-warm-300 bg-white/80 text-sand-800 placeholder:text-sand-400" />
{/* Filters panel */} {showFilters && (
{[ { key: "meuble", label: "Meuble" }, { key: "parking", label: "Parking" }, { key: "balcon", label: "Balcon" }, ].map(({ key, label }) => (
{label}
))}
Prix max setFilters((f) => ({ ...f, maxPrix: e.target.value })) } placeholder="EUR" className="w-20 px-2 py-1 text-xs border border-sand-200 rounded-lg bg-white text-sand-700 focus:outline-none focus:ring-1 focus:ring-warm-300" />
Surface min setFilters((f) => ({ ...f, minSurface: e.target.value })) } placeholder="m2" className="w-20 px-2 py-1 text-xs border border-sand-200 rounded-lg bg-white text-sand-700 focus:outline-none focus:ring-1 focus:ring-warm-300" />
Date
{activeFilterCount > 0 && ( )}
)}
{/* Content */}
{loading && !annonces.length ? (

Chargement des annonces...

) : filtered.length === 0 ? (

Aucune annonce trouvee

{annonces.length ? "Essaie de modifier tes filtres" : "Ajoute ta premiere annonce !"}

) : (
{filtered.map((a, idx) => ( ))}
)}
{/* Add modal */} setShowAdd(false)} onSubmit={handleAdd} existingUrls={existingUrls} />
); } export default App;