Compare commits

...

3 Commits

3 changed files with 195 additions and 26 deletions

View File

@@ -21,6 +21,93 @@ const SORT_OPTIONS = [
{ 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);
@@ -33,6 +120,7 @@ function App() {
balcon: null,
maxPrix: "",
minSurface: "",
dateRange: "",
});
const [showFilters, setShowFilters] = useState(false);
@@ -94,6 +182,9 @@ function App() {
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":
@@ -127,6 +218,7 @@ function App() {
filters.balcon !== null,
!!filters.maxPrix,
!!filters.minSurface,
!!filters.dateRange,
].filter(Boolean).length;
const stats = useMemo(() => {
@@ -302,6 +394,26 @@ function App() {
/>
</div>
<div className="flex items-center gap-1.5">
<span className="text-xs text-sand-600 font-medium">Date</span>
<select
value={filters.dateRange}
onChange={(e) =>
setFilters((f) => ({
...f,
dateRange: e.target.value,
}))
}
className="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_RANGE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
{activeFilterCount > 0 && (
<button
onClick={() =>
@@ -311,6 +423,7 @@ function App() {
balcon: null,
maxPrix: "",
minSurface: "",
dateRange: "",
})
}
className="text-xs text-warm-600 hover:text-warm-800 font-semibold"

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useMemo, useState } from "react";
import { X, Plus, Loader2, Link } from "lucide-react";
export default function AddModal({ open, onClose, onSubmit, existingUrls }) {
@@ -6,11 +6,29 @@ export default function AddModal({ open, onClose, onSubmit, existingUrls }) {
const [loading, setLoading] = useState(false);
const [urlError, setUrlError] = useState("");
const normalizeUrl = (raw) => {
if (!raw) return "";
try {
const parsed = new URL(raw.trim());
parsed.hash = "";
parsed.search = "";
return `${parsed.origin}${parsed.pathname}`;
} catch (err) {
return raw.trim().split(/[?#]/)[0];
}
};
const normalizedExisting = useMemo(
() => existingUrls.map((item) => normalizeUrl(item)),
[existingUrls]
);
if (!open) return null;
const handleUrlChange = (val) => {
setUrl(val);
if (existingUrls.includes(val.trim())) {
const normalized = normalizeUrl(val);
if (normalized && normalizedExisting.includes(normalized)) {
setUrlError("Cette URL existe deja dans la liste !");
} else {
setUrlError("");

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import {
MapPin,
Euro,
@@ -65,12 +65,20 @@ const formatFrDate = (value) => {
date = toDate(value);
if (!date && typeof value === "string") {
const match = value.trim().match(/^([0-9]{1,2})[\/\-]([0-9]{1,2})[\/\-]([0-9]{2,4})$/);
const trimmed = value.trim();
const dayMatch = trimmed.match(/^([0-9]{1,2})[\/\-]([0-9]{1,2})[\/\-]([0-9]{2,4})$/);
if (match) {
const [, day, month, year] = match;
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`);
}
}
}
}
@@ -84,24 +92,63 @@ const formatFrDate = (value) => {
});
};
const normalizeFeedback = (noteP, noteA, commentP, commentA) => ({
NOTE_P: typeof noteP === "number" ? noteP : parseFloat(noteP) || 0,
NOTE_A: typeof noteA === "number" ? noteA : parseFloat(noteA) || 0,
COMMENTAIRE_P: commentP?.trim() ? commentP.trim() : null,
COMMENTAIRE_A: commentA?.trim() ? commentA.trim() : null,
});
export default function AppartCard({ annonce, onDelete, onUpdated }) {
const [noteP, setNoteP] = useState(annonce.NOTE_P || 0);
const [noteA, setNoteA] = useState(annonce.NOTE_A || 0);
const [commentP, setCommentP] = useState(annonce.COMMENTAIRE_P || "");
const [commentA, setCommentA] = useState(annonce.COMMENTAIRE_A || "");
const [saving, setSaving] = useState(false);
const [pendingSave, setPendingSave] = useState(false);
const [expanded, setExpanded] = useState(false);
const [deleting, setDeleting] = useState(false);
const hasChanges =
noteP !== (annonce.NOTE_P || 0) ||
noteA !== (annonce.NOTE_A || 0) ||
commentP !== (annonce.COMMENTAIRE_P || "") ||
commentA !== (annonce.COMMENTAIRE_A || "");
const lastSavedRef = useRef(
normalizeFeedback(noteP, noteA, commentP, commentA)
);
const hasPaulinFeedback = (noteP ?? 0) > 0 || !!commentP?.trim();
const hasAgatheFeedback = (noteA ?? 0) > 0 || !!commentA?.trim();
useEffect(() => {
const normalized = normalizeFeedback(noteP, noteA, commentP, commentA);
const lastSaved = lastSavedRef.current;
if (
normalized.NOTE_P === lastSaved.NOTE_P &&
normalized.NOTE_A === lastSaved.NOTE_A &&
normalized.COMMENTAIRE_P === lastSaved.COMMENTAIRE_P &&
normalized.COMMENTAIRE_A === lastSaved.COMMENTAIRE_A
) {
return;
}
setPendingSave(true);
const timeout = setTimeout(async () => {
setPendingSave(false);
setSaving(true);
try {
await updateAnnonce(annonce.id, normalized);
lastSavedRef.current = normalized;
onUpdated?.();
} catch (err) {
console.error("Erreur sauvegarde annonce", err);
setPendingSave(true);
} finally {
setSaving(false);
}
}, 700);
return () => clearTimeout(timeout);
}, [annonce.id, commentA, commentP, noteA, noteP, onUpdated]);
const handleSave = async () => {
setSaving(true);
await updateAnnonce(annonce.id, {
@@ -225,6 +272,12 @@ export default function AppartCard({ annonce, onDelete, onUpdated }) {
{expanded ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
</button>
{(pendingSave || saving) && (
<div className="px-5 pb-2 text-xs text-sand-400">
{saving ? "Sauvegarde..." : "Sauvegarde automatique imminente..."}
</div>
)}
{expanded && (
<div className="px-5 pb-4 space-y-3">
{/* Paulin */}
@@ -261,21 +314,6 @@ export default function AppartCard({ annonce, onDelete, onUpdated }) {
/>
</div>
{/* Save */}
{hasChanges && (
<button
onClick={handleSave}
disabled={saving}
className="flex items-center gap-1.5 px-4 py-1.5 bg-warm-500 text-white text-sm rounded-xl hover:bg-warm-600 transition-colors disabled:opacity-50 font-medium"
>
{saving ? (
<Loader2 size={14} className="animate-spin" />
) : (
<Save size={14} />
)}
{saving ? "Sauvegarde..." : "Sauvegarder"}
</button>
)}
</div>
)}
</div>