refactor: rebrand application from Paulin & Agathe to Vincent & Antoine and update API endpoint to July webhook

This commit is contained in:
2026-06-02 18:16:21 +02:00
parent 60055dddac
commit 46453604ac
6 changed files with 39 additions and 42 deletions

View File

@@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="description" content="Liste des annonces d'appartements à Nantes | Paulin et Agathe" /> <meta name="description" content="Liste des annonces d'appartements à Nantes | Vincent et Antoine" />
<link rel="icon" href="/logo.png" /> <link rel="icon" href="/logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Appart List</title> <title>Appart List</title>

2
package-lock.json generated
View File

@@ -2525,7 +2525,6 @@
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"jiti": "lib/jiti-cli.mjs" "jiti": "lib/jiti-cli.mjs"
} }
@@ -2627,7 +2626,6 @@
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
"dev": true, "dev": true,
"license": "MPL-2.0", "license": "MPL-2.0",
"peer": true,
"dependencies": { "dependencies": {
"detect-libc": "^2.0.3" "detect-libc": "^2.0.3"
}, },

View File

@@ -199,7 +199,7 @@ function App() {
case "note_desc": case "note_desc":
list.sort( list.sort(
(a, b) => (a, b) =>
(b.NOTE_P || 0) + (b.NOTE_A || 0) - ((a.NOTE_P || 0) + (a.NOTE_A || 0)) (b.NOTE_V || 0) + (b.NOTE_A || 0) - ((a.NOTE_V || 0) + (a.NOTE_A || 0))
); );
break; break;
case "recent": case "recent":
@@ -248,7 +248,7 @@ function App() {
Appart Nantes Appart Nantes
</h1> </h1>
<p className="text-xs text-sand-500"> <p className="text-xs text-sand-500">
Paulin & Agathe Vincent & Antoine
{stats && ( {stats && (
<span className="ml-1.5 text-sand-400"> <span className="ml-1.5 text-sand-400">
-- {stats.count} annonce{stats.count > 1 ? "s" : ""} | moy.{" "} -- {stats.count} annonce{stats.count > 1 ? "s" : ""} | moy.{" "}

View File

@@ -1,4 +1,4 @@
const BASE = "https://n8n.holo795.fr/webhook/annonce_appart"; const BASE = "https://n8n.holo795.fr/webhook/annonce_appart_july";
export async function fetchAnnonces() { export async function fetchAnnonces() {
const res = await fetch(`${BASE}/get`); const res = await fetch(`${BASE}/get`);

View File

@@ -4,8 +4,6 @@ import {
Euro, Euro,
Maximize2, Maximize2,
DoorOpen, DoorOpen,
Bus,
Car,
Calendar, Calendar,
Sofa, Sofa,
ParkingCircle, ParkingCircle,
@@ -92,17 +90,17 @@ const formatFrDate = (value) => {
}); });
}; };
const normalizeFeedback = (noteP, noteA, commentP, commentA) => ({ const normalizeFeedback = (noteV, noteA, commentV, commentA) => ({
NOTE_P: typeof noteP === "number" ? noteP : parseFloat(noteP) || 0, NOTE_V: typeof noteV === "number" ? noteV : parseFloat(noteV) || 0,
NOTE_A: typeof noteA === "number" ? noteA : parseFloat(noteA) || 0, NOTE_A: typeof noteA === "number" ? noteA : parseFloat(noteA) || 0,
COMMENTAIRE_P: commentP?.trim() ? commentP.trim() : null, COMMENTAIRE_V: commentV?.trim() ? commentV.trim() : null,
COMMENTAIRE_A: commentA?.trim() ? commentA.trim() : null, COMMENTAIRE_A: commentA?.trim() ? commentA.trim() : null,
}); });
export default function AppartCard({ annonce, onDelete, onUpdated }) { export default function AppartCard({ annonce, onDelete, onUpdated }) {
const [noteP, setNoteP] = useState(annonce.NOTE_P || 0); const [noteV, setNoteV] = useState(annonce.NOTE_V || 0);
const [noteA, setNoteA] = useState(annonce.NOTE_A || 0); const [noteA, setNoteA] = useState(annonce.NOTE_A || 0);
const [commentP, setCommentP] = useState(annonce.COMMENTAIRE_P || ""); const [commentV, setCommentV] = useState(annonce.COMMENTAIRE_V || "");
const [commentA, setCommentA] = useState(annonce.COMMENTAIRE_A || ""); const [commentA, setCommentA] = useState(annonce.COMMENTAIRE_A || "");
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [pendingSave, setPendingSave] = useState(false); const [pendingSave, setPendingSave] = useState(false);
@@ -110,20 +108,20 @@ export default function AppartCard({ annonce, onDelete, onUpdated }) {
const [deleting, setDeleting] = useState(false); const [deleting, setDeleting] = useState(false);
const lastSavedRef = useRef( const lastSavedRef = useRef(
normalizeFeedback(noteP, noteA, commentP, commentA) normalizeFeedback(noteV, noteA, commentV, commentA)
); );
const hasPaulinFeedback = (noteP ?? 0) > 0 || !!commentP?.trim(); const hasVincentFeedback = (noteV ?? 0) > 0 || !!commentV?.trim();
const hasAgatheFeedback = (noteA ?? 0) > 0 || !!commentA?.trim(); const hasAntoineFeedback = (noteA ?? 0) > 0 || !!commentA?.trim();
useEffect(() => { useEffect(() => {
const normalized = normalizeFeedback(noteP, noteA, commentP, commentA); const normalized = normalizeFeedback(noteV, noteA, commentV, commentA);
const lastSaved = lastSavedRef.current; const lastSaved = lastSavedRef.current;
if ( if (
normalized.NOTE_P === lastSaved.NOTE_P && normalized.NOTE_V === lastSaved.NOTE_V &&
normalized.NOTE_A === lastSaved.NOTE_A && normalized.NOTE_A === lastSaved.NOTE_A &&
normalized.COMMENTAIRE_P === lastSaved.COMMENTAIRE_P && normalized.COMMENTAIRE_V === lastSaved.COMMENTAIRE_V &&
normalized.COMMENTAIRE_A === lastSaved.COMMENTAIRE_A normalized.COMMENTAIRE_A === lastSaved.COMMENTAIRE_A
) { ) {
return; return;
@@ -147,14 +145,14 @@ export default function AppartCard({ annonce, onDelete, onUpdated }) {
}, 700); }, 700);
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
}, [annonce.id, commentA, commentP, noteA, noteP, onUpdated]); }, [annonce.id, commentA, commentV, noteA, noteV, onUpdated]);
const handleSave = async () => { const handleSave = async () => {
setSaving(true); setSaving(true);
await updateAnnonce(annonce.id, { await updateAnnonce(annonce.id, {
NOTE_P: noteP, NOTE_V: noteV,
NOTE_A: noteA, NOTE_A: noteA,
COMMENTAIRE_P: commentP || null, COMMENTAIRE_V: commentV || null,
COMMENTAIRE_A: commentA || null, COMMENTAIRE_A: commentA || null,
}); });
onUpdated?.(); onUpdated?.();
@@ -168,7 +166,7 @@ export default function AppartCard({ annonce, onDelete, onUpdated }) {
setDeleting(false); setDeleting(false);
}; };
const avgNote = noteP || noteA ? ((noteP + noteA) / (noteP && noteA ? 2 : 1)).toFixed(1) : null; const avgNote = noteV || noteA ? ((noteV + noteA) / (noteV && noteA ? 2 : 1)).toFixed(1) : null;
const dateDispo = formatFrDate(annonce.DATE_DISPO); const dateDispo = formatFrDate(annonce.DATE_DISPO);
const dateAjout = formatFrDate(annonce.createdAt); const dateAjout = formatFrDate(annonce.createdAt);
@@ -228,11 +226,12 @@ export default function AppartCard({ annonce, onDelete, onUpdated }) {
<div className="flex flex-wrap gap-x-4 gap-y-1.5"> <div className="flex flex-wrap gap-x-4 gap-y-1.5">
<InfoChip icon={Maximize2} value={`${annonce.SURFACE ?? 0} m2`} /> <InfoChip icon={Maximize2} value={`${annonce.SURFACE ?? 0} m2`} />
<InfoChip icon={DoorOpen} value={`${annonce.NB_PIECE ?? 0} piece${(annonce.NB_PIECE ?? 0) > 1 ? "s" : ""}`} /> <InfoChip icon={DoorOpen} value={`${annonce.NB_PIECE ?? 0} piece${(annonce.NB_PIECE ?? 0) > 1 ? "s" : ""}`} />
{annonce.DISTANCE_V > 0 && ( {annonce.ARRONDISSEMENT && (
<InfoChip icon={Bus} label="Vatel" value={`${annonce.DISTANCE_V} min`} /> <InfoChip
)} icon={MapPin}
{annonce.DISTANCE_S > 0 && ( label="Arrond."
<InfoChip icon={Car} label="Sup de Vinci" value={`${annonce.DISTANCE_S} min`} /> value={`${annonce.ARRONDISSEMENT}${annonce.ARRONDISSEMENT === 1 ? "er" : "e"}`}
/>
)} )}
{dateDispo && <InfoChip icon={Calendar} label="Dispo" value={dateDispo} />} {dateDispo && <InfoChip icon={Calendar} label="Dispo" value={dateDispo} />}
{dateAjout && <InfoChip icon={Calendar} label="Ajout" value={dateAjout} />} {dateAjout && <InfoChip icon={Calendar} label="Ajout" value={dateAjout} />}
@@ -254,14 +253,14 @@ export default function AppartCard({ annonce, onDelete, onUpdated }) {
<span className="flex items-center gap-1.5"> <span className="flex items-center gap-1.5">
<MessageSquare size={14} /> <MessageSquare size={14} />
Notes et commentaires Notes et commentaires
{(hasPaulinFeedback || hasAgatheFeedback) && ( {(hasVincentFeedback || hasAntoineFeedback) && (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
{hasPaulinFeedback && ( {hasVincentFeedback && (
<span className="w-4 h-4 rounded-full bg-warm-500 text-white text-[9px] font-semibold flex items-center justify-center"> <span className="w-4 h-4 rounded-full bg-warm-500 text-white text-[9px] font-semibold flex items-center justify-center">
P V
</span> </span>
)} )}
{hasAgatheFeedback && ( {hasAntoineFeedback && (
<span className="w-4 h-4 rounded-full bg-pink-500 text-white text-[9px] font-semibold flex items-center justify-center"> <span className="w-4 h-4 rounded-full bg-pink-500 text-white text-[9px] font-semibold flex items-center justify-center">
A A
</span> </span>
@@ -280,35 +279,35 @@ export default function AppartCard({ annonce, onDelete, onUpdated }) {
{expanded && ( {expanded && (
<div className="px-5 pb-4 space-y-3"> <div className="px-5 pb-4 space-y-3">
{/* Paulin */} {/* Vincent */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-xs font-semibold text-warm-600 uppercase tracking-wide"> <span className="text-xs font-semibold text-warm-600 uppercase tracking-wide">
Paulin Vincent
</span> </span>
<StarRating value={noteP} onChange={setNoteP} /> <StarRating value={noteV} onChange={setNoteV} />
</div> </div>
<textarea <textarea
value={commentP} value={commentV}
onChange={(e) => setCommentP(e.target.value)} onChange={(e) => setCommentV(e.target.value)}
placeholder="L'avis de Paulin..." placeholder="L'avis de Vincent..."
rows={2} rows={2}
className="w-full px-3 py-2 text-sm border border-sand-200 rounded-xl resize-none focus:outline-none focus:ring-2 focus:ring-warm-200 placeholder:text-sand-300 text-sand-800 bg-sand-50/50" className="w-full px-3 py-2 text-sm border border-sand-200 rounded-xl resize-none focus:outline-none focus:ring-2 focus:ring-warm-200 placeholder:text-sand-300 text-sand-800 bg-sand-50/50"
/> />
</div> </div>
{/* Agathe */} {/* Antoine */}
<div className="space-y-1.5"> <div className="space-y-1.5">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-xs font-semibold text-pink-500 uppercase tracking-wide"> <span className="text-xs font-semibold text-pink-500 uppercase tracking-wide">
Agathe Antoine
</span> </span>
<StarRating value={noteA} onChange={setNoteA} /> <StarRating value={noteA} onChange={setNoteA} />
</div> </div>
<textarea <textarea
value={commentA} value={commentA}
onChange={(e) => setCommentA(e.target.value)} onChange={(e) => setCommentA(e.target.value)}
placeholder="L'avis d'Agathe..." placeholder="L'avis d'Antoine..."
rows={2} rows={2}
className="w-full px-3 py-2 text-sm border border-sand-200 rounded-xl resize-none focus:outline-none focus:ring-2 focus:ring-pink-200 placeholder:text-sand-300 text-sand-800 bg-sand-50/50" className="w-full px-3 py-2 text-sm border border-sand-200 rounded-xl resize-none focus:outline-none focus:ring-2 focus:ring-pink-200 placeholder:text-sand-300 text-sand-800 bg-sand-50/50"
/> />

View File

@@ -39,7 +39,7 @@ export default function PasswordGate({ children }) {
<h1 className="text-2xl font-bold text-sand-900 tracking-tight"> <h1 className="text-2xl font-bold text-sand-900 tracking-tight">
Appart Nantes Appart Nantes
</h1> </h1>
<p className="text-sm text-sand-400 mt-1">Paulin & Agathe</p> <p className="text-sm text-sand-400 mt-1">Vincent & Antoine</p>
</div> </div>
<form <form