diff --git a/src/components/Map/MapView.jsx b/src/components/Map/MapView.jsx index c83e31a..9fdeea6 100644 --- a/src/components/Map/MapView.jsx +++ b/src/components/Map/MapView.jsx @@ -1,20 +1,20 @@ import React, { useEffect, useRef, useState } from "react"; import maplibregl from "maplibre-gl"; -import "maplibre-gl/dist/maplibre-gl.css"; +import "maplibregl/dist/maplibre-gl.css"; import { fetchPosts, fetchIpLocation, createPost } from "../../api/client"; /* ------------ CONSTANTES LOCALSTORAGE -------------- */ const LAST_POS_KEY = "sociowire:lastPosition"; // position utilisateur (GPS/IP) const LAST_VIEW_KEY = "sociowire:lastView"; // dernière vue carte (centre+zoom) -/* ------------ UTILS -------------- */ +/* ------------ UTILS GEOMETRIE -------------- */ -// petite haversine pour estimer le rayon en km en fonction du zoom +// Haversine pour calculer un rayon approx (km) function haversineKm(lat1, lon1, lat2, lon2) { const R = 6371; const toRad = (deg) => (deg * Math.PI) / 180; const dLat = toRad(lat2 - lat1); - const dLon = toRad(lon2 - lon1); + const dLon = toRad(lat2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(toRad(lat1)) * @@ -41,6 +41,7 @@ function getViewFromMap(map) { }; } +// extraction [lng, lat] depuis un post function getCoords(post) { const lat = post.lat ?? @@ -58,6 +59,46 @@ function getCoords(post) { return null; } +/* ------------ UTILS CATEGORIES & TEMPS -------------- */ + +// Boutons de filtre principaux (droite) +const FILTER_MAIN_CATEGORIES = ["All", "News", "Friends", "Events", "Market"]; + +// pour le panneau du bas (sous-cats) – AVEC "All" +const FILTER_CATEGORY_MAP = { + All: ["All"], + News: ["All", "World", "Politics", "Tech", "Finance", "Local"], + Friends: ["All", "Nearby", "Chats", "Groups", "Stories", "Favs"], + Events: ["All", "Today", "Weekend", "Music", "Sports", "Local"], + Market: ["All", "Actu", "Tech", "Finan", "Sport", "Deals"], +}; + +// pour NEW POST seulement (PAS de "All") +const CREATE_CATEGORY_MAP = { + News: ["World", "Politics", "Tech", "Finance", "Local"], + Friends: ["Nearby", "Chats", "Groups", "Stories", "Favs"], + Events: ["Today", "Weekend", "Music", "Sports", "Local"], + Market: ["Actu", "Tech", "Finan", "Sport", "Deals"], +}; + +// map bouton -> code backend +function categoryCode(mainFilter) { + switch (mainFilter) { + case "News": + return "NEWS"; + case "Friends": + return "FRIENDS"; + case "Events": + return "EVENT"; // backend: EVENT + case "Market": + return "MARKET"; + case "All": + default: + return ""; + } +} + +// Transforme un post en petits textes pour les bulles function extract(post) { const title = post.title || post.text || post.body || "Post"; const body = @@ -70,29 +111,55 @@ function extract(post) { return { title, body, short }; } -// map bouton -> code backend -function categoryCode(mainFilter) { - switch (mainFilter) { - case "News": - return "NEWS"; - case "Friends": - return "FRIENDS"; - case "Events": - return "EVENT"; // backend: EVENT (singulier) - case "Market": - return "MARKET"; - default: - return ""; +// sous-catégorie: "All" = tout +function matchesSubFilter(post, subFilter) { + if (!subFilter || subFilter.toUpperCase() === "ALL" || subFilter === "All") { + return true; } + const raw = post.sub_category ?? post.subCategory ?? ""; + const sub = raw.toString().toLowerCase(); + return sub === subFilter.toLowerCase(); } -/* ------------ CATEGORIES POUR LES CHIPS -------------- */ -const CATEGORY_MAP = { - News: ["World", "Politics", "Tech", "Finance", "Local"], - Friends: ["Nearby", "Chats", "Groups", "Stories", "Favs"], - Events: ["Today", "Weekend", "Music", "Sports", "Local"], - Market: ["Actu", "Tech", "Finan", "Sport", "Deals"], -}; +// time filter local – règles : +// Live/NOW = 10 minutes +// Today/TODAY = 24 heures +// Recently/RECENT = 4 heures +// Past/PAST = plus vieux que 24 h +function matchesTimeFilter(createdAt, timeFilter) { + if (!timeFilter) return true; + if (!createdAt) return true; + + let d; + if (typeof createdAt === "string") { + if (createdAt.includes(" ")) { + d = new Date(createdAt.replace(" ", "T") + "Z"); + } else { + d = new Date(createdAt); + } + } else { + d = new Date(createdAt); + } + if (Number.isNaN(d.getTime())) return true; + + const now = new Date(); + const diffMs = now.getTime() - d.getTime(); + const diffMin = diffMs / 60000; + const diffHours = diffMs / 3600000; + + switch (timeFilter) { + case "NOW": // Live = 10 minutes + return diffMin <= 10; + case "TODAY": // 24h + return diffHours <= 24; + case "RECENT": // 4h + return diffHours <= 4; + case "PAST": // plus vieux que 24h + return diffHours > 24; + default: + return true; + } +} /* ===================================================== COMPONENT @@ -103,11 +170,14 @@ export default function MapView() { const markersRef = useRef([]); // [{ id, marker, el, compactHTML, expandedHTML }] const expandedElRef = useRef(null); + // posts bruts (sans filtre temps) pour la zone + cat + sous-cat + const allPostsRef = useRef([]); + const [status, setStatus] = useState("Loading posts..."); - const [mainFilter, setMainFilter] = useState("News"); // News/Friends/Events/Market + const [mainFilter, setMainFilter] = useState("All"); // All/News/Friends/Events/Market const [timeFilter, setTimeFilter] = useState("RECENT"); // NOW/TODAY/RECENT/PAST - const [subFilter, setSubFilter] = useState(null); // sous-catégorie du bas + const [subFilter, setSubFilter] = useState("All"); // "All" ou sous-cat const [userPosition, setUserPosition] = useState(null); // [lng, lat] @@ -118,13 +188,13 @@ export default function MapView() { const [hasLastView, setHasLastView] = useState(false); - // création de post + // NEW POST const [isCreating, setIsCreating] = useState(false); const [draftTitle, setDraftTitle] = useState(""); const [draftBody, setDraftBody] = useState(""); const [draftCategory, setDraftCategory] = useState("News"); const [draftSubCategory, setDraftSubCategory] = useState( - CATEGORY_MAP.News[0] + CREATE_CATEGORY_MAP.News[0] ); const [draftCoords, setDraftCoords] = useState(null); // [lng, lat] const [isSaving, setIsSaving] = useState(false); @@ -195,8 +265,6 @@ export default function MapView() { }); // à chaque fin de move/zoom : - // - on maj viewParams (pour fetch) - // - on sauvegarde la vue dans localStorage map.on("moveend", () => { const vp = getViewFromMap(map); setViewParams(vp); @@ -210,9 +278,7 @@ export default function MapView() { zoom: map.getZoom(), }) ); - } catch (_) { - // ignore - } + } catch (_) {} }); return () => { @@ -297,16 +363,16 @@ export default function MapView() { }, [hasLastView]); /* ------------ SOUS-CATEGORIES (bas) -------------- */ - const bottomCategories = CATEGORY_MAP[mainFilter] || []; + const bottomCategories = FILTER_CATEGORY_MAP[mainFilter] || ["All"]; useEffect(() => { if (!bottomCategories.length) return; if (!subFilter || !bottomCategories.includes(subFilter)) { - setSubFilter(bottomCategories[0]); + setSubFilter("All"); } }, [mainFilter, bottomCategories, subFilter]); - /* ------------ FACTEUR COMMUN: CREATION D'UN MARKER POUR UN POST -------------- */ + /* ------------ CRÉATION D'UN MARKER POUR UN POST -------------- */ const createMarkerForPost = (post) => { const map = mapRef.current; if (!map) return; @@ -365,7 +431,7 @@ export default function MapView() { .addTo(map); const meta = { - id: post.id ?? null, + id: typeof post.id === "number" ? post.id : null, marker, el, compactHTML, @@ -402,7 +468,30 @@ export default function MapView() { }); }; - /* ------------ LOAD POSTS POUR LA ZONE COURANTE + FILTRES -------------- */ + /* ------------ RECONSTRUIRE LES MARKERS SELON LE TIME FILTER -------------- */ + const rebuildMarkersForTimeFilter = (tf) => { + const map = mapRef.current; + if (!map) return; + + // on enlève tous les markers + markersRef.current.forEach((m) => m.marker.remove()); + markersRef.current = []; + expandedElRef.current = null; + + const posts = allPostsRef.current || []; + + const visible = posts.filter((p) => + matchesTimeFilter(p.created_at || p.CreatedAt, tf) + ); + + visible.forEach((post) => { + createMarkerForPost(post); + }); + + setStatus(visible.length ? "" : "No posts found."); + }; + + /* ------------ LOAD POSTS POUR LA ZONE COURANTE + CAT + SOUS-CAT -------------- */ useEffect(() => { const map = mapRef.current; if (!map) return; @@ -419,12 +508,15 @@ export default function MapView() { const catCode = categoryCode(mainFilter); const subCatParam = - subFilter && subFilter.toUpperCase() !== "ALL" ? subFilter : ""; + subFilter && subFilter.toUpperCase() !== "ALL" && subFilter !== "All" + ? subFilter + : ""; + // on laisse "time" vide -> le backend renvoie tout pour cette zone/cat/sous-cat const posts = await fetchPosts({ category: catCode, subCategory: subCatParam, - time: timeFilter, + time: "", lat, lon: lng, radiusKm, @@ -432,17 +524,10 @@ export default function MapView() { if (cancelled) return; - // *** ICI: les filtres contrôlent TOUT *** - // On efface tous les markers, et on remet EXACTEMENT ce que le backend renvoie. - markersRef.current.forEach((m) => m.marker.remove()); - markersRef.current = []; - expandedElRef.current = null; + allPostsRef.current = Array.isArray(posts) ? posts : []; - posts.forEach((post) => { - createMarkerForPost(post); - }); - - setStatus(posts.length ? "" : "No posts found."); + // Puis on reconstruit les markers avec le timeFilter ACTUEL + rebuildMarkersForTimeFilter(timeFilter); } catch (err) { console.error("Erreur chargement posts:", err); if (!cancelled) setStatus("Error loading posts."); @@ -454,9 +539,16 @@ export default function MapView() { return () => { cancelled = true; }; - }, [viewParams, mainFilter, timeFilter, subFilter]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [viewParams, mainFilter, subFilter]); - /* ------------ WEBSOCKET : AJOUTER JUSTE LE NOUVEAU POST -------------- */ + /* ------------ QUAND LE TIME FILTER CHANGE, ON RECONSTRUIT JUSTE LES MARKERS -------------- */ + useEffect(() => { + rebuildMarkersForTimeFilter(timeFilter); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [timeFilter]); + + /* ------------ WEBSOCKET : AJOUTE JUSTE LES POSTS QUI MATCH LES FILTRES -------------- */ useEffect(() => { const proto = window.location.protocol === "https:" ? "wss" : "ws"; const host = @@ -480,18 +572,25 @@ export default function MapView() { if (msg.type === "new_post" && msg.post) { const p = msg.post; - // Respecte les filtres actuels - - // 1) CATEGORY + // 1) catégorie const catCode = categoryCode(mainFilter); - if (catCode && p.category && p.category.toUpperCase() !== catCode) { + if ( + catCode && + p.category && + p.category.toUpperCase() !== catCode + ) { return; } - // 2) TIME filter : on fait confiance au backend (p.created_at récent) - // Si ton backend ne renvoie pas created_at, on laisse passer. + // 2) sous-catégorie + if (!matchesSubFilter(p, subFilter)) return; - // 3) ZONE: ne l'afficher que s'il est dans le rayon courant + // 3) temps (10 min / 24h / 4h / >24h) + if (!matchesTimeFilter(p.created_at || p.CreatedAt, timeFilter)) { + return; + } + + // 4) zone actuelle if (viewParams.center && viewParams.radiusKm) { const coords = getCoords(p); if (coords) { @@ -499,13 +598,24 @@ export default function MapView() { const [lngC, latC] = viewParams.center; const dKm = haversineKm(latC, lngC, latP, lngP); if (dKm > viewParams.radiusKm * 1.2) { - // trop loin de la vue actuelle → on ignore return; } } } - // On ajoute juste ce post, sans effacer les autres + // 5) éviter doublon (dans la liste brute) + const id = typeof p.id === "number" ? p.id : null; + if ( + id !== null && + allPostsRef.current.some((post) => post.id === id) + ) { + return; + } + + // 6) ajouter à la liste brute + allPostsRef.current = [...allPostsRef.current, p]; + + // 7) et si il match le timeFilter courant, on l'ajoute visuellement createMarkerForPost(p); } } catch (_) { @@ -522,7 +632,7 @@ export default function MapView() { ws.close(); } }; - }, [mainFilter, timeFilter, viewParams]); + }, [mainFilter, subFilter, timeFilter, viewParams]); /* ------------ ACTIONS UI -------------- */ @@ -551,9 +661,10 @@ export default function MapView() { setDraftTitle(""); setDraftBody(""); - const main = mainFilter || "News"; + // pour NEW POST on n'a PAS de catégorie "All" + const main = mainFilter === "All" ? "News" : mainFilter; setDraftCategory(main); - const list = CATEGORY_MAP[main] || CATEGORY_MAP.News; + const list = CREATE_CATEGORY_MAP[main] || CREATE_CATEGORY_MAP.News; setDraftSubCategory(list[0]); setDraftCoords(null); }; @@ -603,7 +714,7 @@ export default function MapView() { await createPost(payload); setIsSaving(false); setIsCreating(false); - // On laisse le WS rajouter le nouveau + // le WS va pousser le nouveau post si il match les filtres } catch (e) { console.error("createPost error:", e); setIsSaving(false); @@ -632,12 +743,12 @@ export default function MapView() { - {/* LEFT — temps */} + {/* LEFT — time filters */}