Auto deploy: 2025-12-09 04:21:25
This commit is contained in:
parent
9814fc411a
commit
ecf2e54778
|
|
@ -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() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{/* LEFT — temps */}
|
||||
{/* LEFT — time filters */}
|
||||
<div className="map-overlay map-overlay-left">
|
||||
{[
|
||||
["NOW", "Now"],
|
||||
["NOW", "Live"],
|
||||
["TODAY", "Today"],
|
||||
["RECENT", "Recent"],
|
||||
["RECENT", "Recently"],
|
||||
["PAST", "Past"],
|
||||
].map(([code, label]) => (
|
||||
<button
|
||||
|
|
@ -652,9 +763,9 @@ export default function MapView() {
|
|||
))}
|
||||
</div>
|
||||
|
||||
{/* RIGHT — filtres principaux */}
|
||||
{/* RIGHT — catégories principales avec All */}
|
||||
<div className="map-overlay map-overlay-right">
|
||||
{["News", "Friends", "Events", "Market"].map((cat) => (
|
||||
{FILTER_MAIN_CATEGORIES.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
className={
|
||||
|
|
@ -667,7 +778,7 @@ export default function MapView() {
|
|||
))}
|
||||
</div>
|
||||
|
||||
{/* BOTTOM — arc courbé inversé autour du globe */}
|
||||
{/* BOTTOM — sous-cats + All */}
|
||||
<div className="map-overlay map-overlay-bottom">
|
||||
{bottomCategories.map((c) => (
|
||||
<button
|
||||
|
|
@ -693,14 +804,14 @@ export default function MapView() {
|
|||
</button>
|
||||
</div>
|
||||
|
||||
{/* CROSSHAIR SUR LA MAP (au-dessus du popup) */}
|
||||
{/* CROSSHAIR SUR LA MAP */}
|
||||
{isCreating && (
|
||||
<div className="map-overlay map-crosshair" onClick={handlePickCenter}>
|
||||
<div className="crosshair-aim" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* PANEL CREATION DE POST (1/5 du bas) */}
|
||||
{/* PANEL CREATION DE POST */}
|
||||
{isCreating && (
|
||||
<div className="map-overlay create-post-panel">
|
||||
<div className="create-post-header">
|
||||
|
|
@ -727,7 +838,8 @@ export default function MapView() {
|
|||
onChange={(e) => {
|
||||
const cat = e.target.value;
|
||||
setDraftCategory(cat);
|
||||
const list = CATEGORY_MAP[cat] || CATEGORY_MAP.News;
|
||||
const list =
|
||||
CREATE_CATEGORY_MAP[cat] || CREATE_CATEGORY_MAP.News;
|
||||
setDraftSubCategory(list[0]);
|
||||
}}
|
||||
>
|
||||
|
|
@ -742,13 +854,13 @@ export default function MapView() {
|
|||
value={draftSubCategory}
|
||||
onChange={(e) => setDraftSubCategory(e.target.value)}
|
||||
>
|
||||
{(CATEGORY_MAP[draftCategory] || CATEGORY_MAP.News).map(
|
||||
(sub) => (
|
||||
<option key={sub} value={sub}>
|
||||
{sub}
|
||||
</option>
|
||||
)
|
||||
)}
|
||||
{(CREATE_CATEGORY_MAP[draftCategory] ||
|
||||
CREATE_CATEGORY_MAP.News
|
||||
).map((sub) => (
|
||||
<option key={sub} value={sub}>
|
||||
{sub}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue