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 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>