Auto deploy: 2025-12-09 04:21:25

This commit is contained in:
Your Name 2025-12-09 04:21:28 +00:00
parent 9814fc411a
commit ecf2e54778
1 changed files with 193 additions and 81 deletions

View File

@ -1,20 +1,20 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import maplibregl from "maplibre-gl"; 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"; import { fetchPosts, fetchIpLocation, createPost } from "../../api/client";
/* ------------ CONSTANTES LOCALSTORAGE -------------- */ /* ------------ CONSTANTES LOCALSTORAGE -------------- */
const LAST_POS_KEY = "sociowire:lastPosition"; // position utilisateur (GPS/IP) const LAST_POS_KEY = "sociowire:lastPosition"; // position utilisateur (GPS/IP)
const LAST_VIEW_KEY = "sociowire:lastView"; // dernière vue carte (centre+zoom) 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) { function haversineKm(lat1, lon1, lat2, lon2) {
const R = 6371; const R = 6371;
const toRad = (deg) => (deg * Math.PI) / 180; const toRad = (deg) => (deg * Math.PI) / 180;
const dLat = toRad(lat2 - lat1); const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1); const dLon = toRad(lat2 - lon1);
const a = const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat1)) *
@ -41,6 +41,7 @@ function getViewFromMap(map) {
}; };
} }
// extraction [lng, lat] depuis un post
function getCoords(post) { function getCoords(post) {
const lat = const lat =
post.lat ?? post.lat ??
@ -58,6 +59,46 @@ function getCoords(post) {
return null; 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) { function extract(post) {
const title = post.title || post.text || post.body || "Post"; const title = post.title || post.text || post.body || "Post";
const body = const body =
@ -70,29 +111,55 @@ function extract(post) {
return { title, body, short }; return { title, body, short };
} }
// map bouton -> code backend // sous-catégorie: "All" = tout
function categoryCode(mainFilter) { function matchesSubFilter(post, subFilter) {
switch (mainFilter) { if (!subFilter || subFilter.toUpperCase() === "ALL" || subFilter === "All") {
case "News": return true;
return "NEWS";
case "Friends":
return "FRIENDS";
case "Events":
return "EVENT"; // backend: EVENT (singulier)
case "Market":
return "MARKET";
default:
return "";
} }
const raw = post.sub_category ?? post.subCategory ?? "";
const sub = raw.toString().toLowerCase();
return sub === subFilter.toLowerCase();
} }
/* ------------ CATEGORIES POUR LES CHIPS -------------- */ // time filter local règles :
const CATEGORY_MAP = { // Live/NOW = 10 minutes
News: ["World", "Politics", "Tech", "Finance", "Local"], // Today/TODAY = 24 heures
Friends: ["Nearby", "Chats", "Groups", "Stories", "Favs"], // Recently/RECENT = 4 heures
Events: ["Today", "Weekend", "Music", "Sports", "Local"], // Past/PAST = plus vieux que 24 h
Market: ["Actu", "Tech", "Finan", "Sport", "Deals"], 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 COMPONENT
@ -103,11 +170,14 @@ export default function MapView() {
const markersRef = useRef([]); // [{ id, marker, el, compactHTML, expandedHTML }] const markersRef = useRef([]); // [{ id, marker, el, compactHTML, expandedHTML }]
const expandedElRef = useRef(null); 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 [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 [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] const [userPosition, setUserPosition] = useState(null); // [lng, lat]
@ -118,13 +188,13 @@ export default function MapView() {
const [hasLastView, setHasLastView] = useState(false); const [hasLastView, setHasLastView] = useState(false);
// création de post // NEW POST
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [draftTitle, setDraftTitle] = useState(""); const [draftTitle, setDraftTitle] = useState("");
const [draftBody, setDraftBody] = useState(""); const [draftBody, setDraftBody] = useState("");
const [draftCategory, setDraftCategory] = useState("News"); const [draftCategory, setDraftCategory] = useState("News");
const [draftSubCategory, setDraftSubCategory] = useState( const [draftSubCategory, setDraftSubCategory] = useState(
CATEGORY_MAP.News[0] CREATE_CATEGORY_MAP.News[0]
); );
const [draftCoords, setDraftCoords] = useState(null); // [lng, lat] const [draftCoords, setDraftCoords] = useState(null); // [lng, lat]
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
@ -195,8 +265,6 @@ export default function MapView() {
}); });
// à chaque fin de move/zoom : // à chaque fin de move/zoom :
// - on maj viewParams (pour fetch)
// - on sauvegarde la vue dans localStorage
map.on("moveend", () => { map.on("moveend", () => {
const vp = getViewFromMap(map); const vp = getViewFromMap(map);
setViewParams(vp); setViewParams(vp);
@ -210,9 +278,7 @@ export default function MapView() {
zoom: map.getZoom(), zoom: map.getZoom(),
}) })
); );
} catch (_) { } catch (_) {}
// ignore
}
}); });
return () => { return () => {
@ -297,16 +363,16 @@ export default function MapView() {
}, [hasLastView]); }, [hasLastView]);
/* ------------ SOUS-CATEGORIES (bas) -------------- */ /* ------------ SOUS-CATEGORIES (bas) -------------- */
const bottomCategories = CATEGORY_MAP[mainFilter] || []; const bottomCategories = FILTER_CATEGORY_MAP[mainFilter] || ["All"];
useEffect(() => { useEffect(() => {
if (!bottomCategories.length) return; if (!bottomCategories.length) return;
if (!subFilter || !bottomCategories.includes(subFilter)) { if (!subFilter || !bottomCategories.includes(subFilter)) {
setSubFilter(bottomCategories[0]); setSubFilter("All");
} }
}, [mainFilter, bottomCategories, subFilter]); }, [mainFilter, bottomCategories, subFilter]);
/* ------------ FACTEUR COMMUN: CREATION D'UN MARKER POUR UN POST -------------- */ /* ------------ CRÉATION D'UN MARKER POUR UN POST -------------- */
const createMarkerForPost = (post) => { const createMarkerForPost = (post) => {
const map = mapRef.current; const map = mapRef.current;
if (!map) return; if (!map) return;
@ -365,7 +431,7 @@ export default function MapView() {
.addTo(map); .addTo(map);
const meta = { const meta = {
id: post.id ?? null, id: typeof post.id === "number" ? post.id : null,
marker, marker,
el, el,
compactHTML, 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(() => { useEffect(() => {
const map = mapRef.current; const map = mapRef.current;
if (!map) return; if (!map) return;
@ -419,12 +508,15 @@ export default function MapView() {
const catCode = categoryCode(mainFilter); const catCode = categoryCode(mainFilter);
const subCatParam = 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({ const posts = await fetchPosts({
category: catCode, category: catCode,
subCategory: subCatParam, subCategory: subCatParam,
time: timeFilter, time: "",
lat, lat,
lon: lng, lon: lng,
radiusKm, radiusKm,
@ -432,17 +524,10 @@ export default function MapView() {
if (cancelled) return; if (cancelled) return;
// *** ICI: les filtres contrôlent TOUT *** allPostsRef.current = Array.isArray(posts) ? posts : [];
// 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;
posts.forEach((post) => { // Puis on reconstruit les markers avec le timeFilter ACTUEL
createMarkerForPost(post); rebuildMarkersForTimeFilter(timeFilter);
});
setStatus(posts.length ? "" : "No posts found.");
} catch (err) { } catch (err) {
console.error("Erreur chargement posts:", err); console.error("Erreur chargement posts:", err);
if (!cancelled) setStatus("Error loading posts."); if (!cancelled) setStatus("Error loading posts.");
@ -454,9 +539,16 @@ export default function MapView() {
return () => { return () => {
cancelled = true; 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(() => { useEffect(() => {
const proto = window.location.protocol === "https:" ? "wss" : "ws"; const proto = window.location.protocol === "https:" ? "wss" : "ws";
const host = const host =
@ -480,18 +572,25 @@ export default function MapView() {
if (msg.type === "new_post" && msg.post) { if (msg.type === "new_post" && msg.post) {
const p = msg.post; const p = msg.post;
// Respecte les filtres actuels // 1) catégorie
// 1) CATEGORY
const catCode = categoryCode(mainFilter); const catCode = categoryCode(mainFilter);
if (catCode && p.category && p.category.toUpperCase() !== catCode) { if (
catCode &&
p.category &&
p.category.toUpperCase() !== catCode
) {
return; return;
} }
// 2) TIME filter : on fait confiance au backend (p.created_at récent) // 2) sous-catégorie
// Si ton backend ne renvoie pas created_at, on laisse passer. 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) { if (viewParams.center && viewParams.radiusKm) {
const coords = getCoords(p); const coords = getCoords(p);
if (coords) { if (coords) {
@ -499,13 +598,24 @@ export default function MapView() {
const [lngC, latC] = viewParams.center; const [lngC, latC] = viewParams.center;
const dKm = haversineKm(latC, lngC, latP, lngP); const dKm = haversineKm(latC, lngC, latP, lngP);
if (dKm > viewParams.radiusKm * 1.2) { if (dKm > viewParams.radiusKm * 1.2) {
// trop loin de la vue actuelle on ignore
return; 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); createMarkerForPost(p);
} }
} catch (_) { } catch (_) {
@ -522,7 +632,7 @@ export default function MapView() {
ws.close(); ws.close();
} }
}; };
}, [mainFilter, timeFilter, viewParams]); }, [mainFilter, subFilter, timeFilter, viewParams]);
/* ------------ ACTIONS UI -------------- */ /* ------------ ACTIONS UI -------------- */
@ -551,9 +661,10 @@ export default function MapView() {
setDraftTitle(""); setDraftTitle("");
setDraftBody(""); setDraftBody("");
const main = mainFilter || "News"; // pour NEW POST on n'a PAS de catégorie "All"
const main = mainFilter === "All" ? "News" : mainFilter;
setDraftCategory(main); setDraftCategory(main);
const list = CATEGORY_MAP[main] || CATEGORY_MAP.News; const list = CREATE_CATEGORY_MAP[main] || CREATE_CATEGORY_MAP.News;
setDraftSubCategory(list[0]); setDraftSubCategory(list[0]);
setDraftCoords(null); setDraftCoords(null);
}; };
@ -603,7 +714,7 @@ export default function MapView() {
await createPost(payload); await createPost(payload);
setIsSaving(false); setIsSaving(false);
setIsCreating(false); setIsCreating(false);
// On laisse le WS rajouter le nouveau // le WS va pousser le nouveau post si il match les filtres
} catch (e) { } catch (e) {
console.error("createPost error:", e); console.error("createPost error:", e);
setIsSaving(false); setIsSaving(false);
@ -632,12 +743,12 @@ export default function MapView() {
</button> </button>
</div> </div>
{/* LEFT — temps */} {/* LEFT — time filters */}
<div className="map-overlay map-overlay-left"> <div className="map-overlay map-overlay-left">
{[ {[
["NOW", "Now"], ["NOW", "Live"],
["TODAY", "Today"], ["TODAY", "Today"],
["RECENT", "Recent"], ["RECENT", "Recently"],
["PAST", "Past"], ["PAST", "Past"],
].map(([code, label]) => ( ].map(([code, label]) => (
<button <button
@ -652,9 +763,9 @@ export default function MapView() {
))} ))}
</div> </div>
{/* RIGHT — filtres principaux */} {/* RIGHT — catégories principales avec All */}
<div className="map-overlay map-overlay-right"> <div className="map-overlay map-overlay-right">
{["News", "Friends", "Events", "Market"].map((cat) => ( {FILTER_MAIN_CATEGORIES.map((cat) => (
<button <button
key={cat} key={cat}
className={ className={
@ -667,7 +778,7 @@ export default function MapView() {
))} ))}
</div> </div>
{/* BOTTOM — arc courbé inversé autour du globe */} {/* BOTTOM — sous-cats + All */}
<div className="map-overlay map-overlay-bottom"> <div className="map-overlay map-overlay-bottom">
{bottomCategories.map((c) => ( {bottomCategories.map((c) => (
<button <button
@ -693,14 +804,14 @@ export default function MapView() {
</button> </button>
</div> </div>
{/* CROSSHAIR SUR LA MAP (au-dessus du popup) */} {/* CROSSHAIR SUR LA MAP */}
{isCreating && ( {isCreating && (
<div className="map-overlay map-crosshair" onClick={handlePickCenter}> <div className="map-overlay map-crosshair" onClick={handlePickCenter}>
<div className="crosshair-aim" /> <div className="crosshair-aim" />
</div> </div>
)} )}
{/* PANEL CREATION DE POST (1/5 du bas) */} {/* PANEL CREATION DE POST */}
{isCreating && ( {isCreating && (
<div className="map-overlay create-post-panel"> <div className="map-overlay create-post-panel">
<div className="create-post-header"> <div className="create-post-header">
@ -727,7 +838,8 @@ export default function MapView() {
onChange={(e) => { onChange={(e) => {
const cat = e.target.value; const cat = e.target.value;
setDraftCategory(cat); setDraftCategory(cat);
const list = CATEGORY_MAP[cat] || CATEGORY_MAP.News; const list =
CREATE_CATEGORY_MAP[cat] || CREATE_CATEGORY_MAP.News;
setDraftSubCategory(list[0]); setDraftSubCategory(list[0]);
}} }}
> >
@ -742,13 +854,13 @@ export default function MapView() {
value={draftSubCategory} value={draftSubCategory}
onChange={(e) => setDraftSubCategory(e.target.value)} onChange={(e) => setDraftSubCategory(e.target.value)}
> >
{(CATEGORY_MAP[draftCategory] || CATEGORY_MAP.News).map( {(CREATE_CATEGORY_MAP[draftCategory] ||
(sub) => ( CREATE_CATEGORY_MAP.News
<option key={sub} value={sub}> ).map((sub) => (
{sub} <option key={sub} value={sub}>
</option> {sub}
) </option>
)} ))}
</select> </select>
</div> </div>