From 174f3784e3ecbd936eff049c02b9f7a7234110ad Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 9 Dec 2025 03:18:04 +0000 Subject: [PATCH] Auto deploy: 2025-12-09 03:18:01 --- fix-maplibre-css.sh | 19 ++ src/api/client.js | 122 +++++-- src/components/Map/MapView.jsx | 596 ++++++++++++++++++++++++++++----- src/styles/overlays.css | 165 ++++++++- 4 files changed, 764 insertions(+), 138 deletions(-) create mode 100755 fix-maplibre-css.sh diff --git a/fix-maplibre-css.sh b/fix-maplibre-css.sh new file mode 100755 index 0000000..8954834 --- /dev/null +++ b/fix-maplibre-css.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +echo "Checking for maplibre-gl.css..." + +if [ -f node_modules/maplibre-gl/dist/maplibre-gl.css ]; then + CSS="maplibre-gl/dist/maplibre-gl.css" +elif [ -f node_modules/maplibre-gl/maplibre-gl.css ]; then + CSS="maplibre-gl/maplibre-gl.css" +else + echo "❌ Aucun fichier CSS trouvé dans maplibre-gl" + exit 1 +fi + +echo "✔ CSS found at: $CSS" +echo "✔ Updating MapView.jsx..." + +sed -i "s|import .*maplibre.*css.*|import \"$CSS\";|" src/components/Map/MapView.jsx + +echo "✔ Done!" diff --git a/src/api/client.js b/src/api/client.js index ec901b9..c191060 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -1,13 +1,36 @@ /** - * SocioWire API client - * Frontend -> Backend (port 8081 via proxy /api) + * SocioWire frontend API client */ const API_BASE = "/api"; -/** Récupère la liste des posts */ -export async function fetchPosts() { - const res = await fetch(`${API_BASE}/posts`, { +/** + * Récupère la liste des posts avec filtres. + * filters: { + * category?: "NEWS" | "FRIENDS" | "EVENTS" | "MARKET" + * subCategory?: string + * time?: "NOW" | "TODAY" | "RECENT" | "PAST" + * lat?: number + * lon?: number + * radiusKm?: number + * } + */ +export async function fetchPosts(filters = {}) { + const params = new URLSearchParams(); + + if (filters.category) params.set("category", filters.category); + if (filters.subCategory) params.set("sub_category", filters.subCategory); + if (filters.time) params.set("time", filters.time); + if (typeof filters.lat === "number") params.set("lat", String(filters.lat)); + if (typeof filters.lon === "number") params.set("lon", String(filters.lon)); + if (typeof filters.radiusKm === "number") { + params.set("radius_km", String(filters.radiusKm)); + } + + const qs = params.toString(); + const url = qs ? `${API_BASE}/posts?${qs}` : `${API_BASE}/posts`; + + const res = await fetch(url, { credentials: "include", }); @@ -21,39 +44,72 @@ export async function fetchPosts() { } /** - * Récupère la position approx de l'utilisateur. - * Le backend doit renvoyer { lat, lon } ou { latitude, longitude } etc. + * Récupère la position approx de l'utilisateur depuis le backend. + * Backend: GET /api/ip-location → { lat: number, lon: number } ou "null" */ -export async function fetchUserLocation() { - const res = await fetch(`${API_BASE}/location`, { +export async function fetchIpLocation() { + try { + const res = await fetch(`${API_BASE}/ip-location`, { + credentials: "include", + }); + + if (!res.ok) { + console.warn("fetchIpLocation: HTTP", res.status); + return null; + } + + const text = await res.text(); + if (!text || text === "null") { + return null; + } + + let data; + try { + data = JSON.parse(text); + } catch (e) { + console.warn("fetchIpLocation: JSON parse error", e, text); + return null; + } + + const lat = data.lat; + const lon = data.lon; + + if ( + typeof lat === "number" && + typeof lon === "number" && + !Number.isNaN(lat) && + !Number.isNaN(lon) + ) { + return [lon, lat]; // [lng, lat] + } + + console.warn("fetchIpLocation: invalid payload", data); + return null; + } catch (err) { + console.warn("fetchIpLocation: network error", err); + return null; + } +} + +/** + * Crée un nouveau post + * Backend: POST /api/post (postCreateHandler) + */ +export async function createPost(payload) { + const res = await fetch(`${API_BASE}/post`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, credentials: "include", + body: JSON.stringify(payload), }); if (!res.ok) { - console.warn("fetchUserLocation failed", res.status); - return null; + const text = await res.text().catch(() => ""); + console.error("createPost failed", res.status, text); + throw new Error("Failed to create post"); } - const data = await res.json(); - - const lat = - data.lat ?? - data.latitude ?? - data.location?.lat ?? - data.location?.latitude; - - const lon = - data.lon ?? - data.lng ?? - data.longitude ?? - data.location?.lon ?? - data.location?.lng ?? - data.location?.longitude; - - if (typeof lat === "number" && typeof lon === "number") { - return [lon, lat]; // [lng, lat] pour MapLibre - } - - console.warn("fetchUserLocation: invalid payload", data); - return null; + return res.json().catch(() => ({})); } diff --git a/src/components/Map/MapView.jsx b/src/components/Map/MapView.jsx index 5526b1d..4a58a1a 100644 --- a/src/components/Map/MapView.jsx +++ b/src/components/Map/MapView.jsx @@ -1,9 +1,44 @@ import React, { useEffect, useRef, useState } from "react"; import maplibregl from "maplibre-gl"; import "maplibre-gl/dist/maplibre-gl.css"; -import { fetchPosts } from "../../api/client"; +import { fetchPosts, fetchIpLocation, createPost } from "../../api/client"; /* ------------ UTILS -------------- */ +const LAST_POS_KEY = "sociowire:lastPosition"; // position utilisateur (GPS/IP) +const LAST_VIEW_KEY = "sociowire:lastView"; // dernière vue carte (centre+zoom) + +// petite haversine pour estimer le rayon en km en fonction du zoom +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 a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(toRad(lat1)) * + Math.cos(toRad(lat2)) * + Math.sin(dLon / 2) * + Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; +} + +// centre + rayon (km) de la vue actuelle +function getViewFromMap(map) { + const bounds = map.getBounds(); + const center = bounds.getCenter(); + const ne = bounds.getNorthEast(); + let radiusKm = haversineKm(center.lat, center.lng, ne.lat, ne.lng); + if (!Number.isFinite(radiusKm) || radiusKm <= 0) { + radiusKm = 250; + } + radiusKm = Math.min(Math.max(radiusKm, 10), 1000); // clamp 10–1000 km + return { + center: [center.lng, center.lat], + radiusKm, + }; +} + function getCoords(post) { const lat = post.lat ?? @@ -24,12 +59,32 @@ function getCoords(post) { function extract(post) { const title = post.title || post.text || post.body || "Post"; const body = - post.body || post.content || post.text || "No additional details."; + post.snippet || + post.body || + post.content || + post.text || + "No additional details."; const short = title.length > 28 ? title.slice(0, 25) + "..." : title; return { title, body, short }; } -/* ------------ BOTTOM CATEGORIES -------------- */ +// 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 ""; + } +} + +/* ------------ CATEGORIES -------------- */ const CATEGORY_MAP = { News: ["World", "Politics", "Tech", "Finance", "Local"], Friends: ["Nearby", "Chats", "Groups", "Stories", "Favs"], @@ -41,12 +96,36 @@ const CATEGORY_MAP = { export default function MapView() { const containerRef = useRef(null); const mapRef = useRef(null); - const markersRef = useRef([]); // { marker, el, compactHTML, expandedHTML } + const markersRef = useRef([]); const expandedElRef = useRef(null); const [status, setStatus] = useState("Loading posts..."); - const [mapReady, setMapReady] = useState(false); - const [mainFilter, setMainFilter] = useState("Default"); + + const [mainFilter, setMainFilter] = useState("Default"); // News/Friends/Events/Market + const [timeFilter, setTimeFilter] = useState("RECENT"); // NOW/TODAY/RECENT/PAST + const [subFilter, setSubFilter] = useState(null); // sous-catégorie du bas + + const [userPosition, setUserPosition] = useState(null); // [lng, lat] + const [reloadToken, setReloadToken] = useState(0); + + const [viewParams, setViewParams] = useState({ + center: null, + radiusKm: 250, + }); + + const [hasLastView, setHasLastView] = useState(false); + + // création de 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] + ); + const [draftCoords, setDraftCoords] = useState(null); // [lng, lat] + const [isSaving, setIsSaving] = useState(false); + const [saveError, setSaveError] = useState(""); /* ------------ INIT MAP -------------- */ useEffect(() => { @@ -55,17 +134,63 @@ export default function MapView() { const map = new maplibregl.Map({ container: containerRef.current, style: "https://demotiles.maplibre.org/style.json", - center: [-70, 47], - zoom: 5, + center: [-95, 40], + zoom: 3.5, pitch: 45, - bearing: -20, + bearing: -15, antialias: true, }); - map.addControl(new maplibregl.NavigationControl(), "top-right"); + // si on a une dernière vue en localStorage, on la remet ici + let hadLastView = false; + try { + const raw = localStorage.getItem(LAST_VIEW_KEY); + if (raw) { + const v = JSON.parse(raw); + if ( + typeof v.lat === "number" && + typeof v.lon === "number" && + typeof v.zoom === "number" + ) { + map.setCenter([v.lon, v.lat]); + map.setZoom(v.zoom); + hadLastView = true; + } + } + } catch (_) { + // ignore + } + setHasLastView(hadLastView); + map.addControl(new maplibregl.NavigationControl(), "top-right"); mapRef.current = map; - setMapReady(true); + + // première vue → viewParams + map.on("load", () => { + const vp = getViewFromMap(map); + setViewParams(vp); + }); + + // à 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); + try { + const center = vp.center; + localStorage.setItem( + LAST_VIEW_KEY, + JSON.stringify({ + lat: center[1], + lon: center[0], + zoom: map.getZoom(), + }) + ); + } catch (_) { + // ignore + } + }); return () => { markersRef.current.forEach((m) => m.marker.remove()); @@ -75,75 +200,117 @@ export default function MapView() { }; }, []); - /* ------------ CENTRER SUR POSITION USER -------------- */ - useEffect(() => { - if (!mapReady || !mapRef.current) return; - - const map = mapRef.current; - let cancelled = false; - - function flyTo(lat, lon) { - if (cancelled) return; - map.flyTo({ - center: [lon, lat], - zoom: 10, - speed: 1.4, - curve: 1.6, - }); - } - - async function fallbackIp() { - try { - const res = await fetch("/api/location"); // <-- adapte si ton endpoint est différent - if (!res.ok) return; - const data = await res.json(); // attendu: { lat: number, lon: number } - if ( - typeof data.lat === "number" && - typeof data.lon === "number" && - !Number.isNaN(data.lat) && - !Number.isNaN(data.lon) - ) { - flyTo(data.lat, data.lon); - } - } catch (e) { - // pas grave, on garde le centre par défaut - console.warn("IP geo fallback failed:", e); - } - } - - if ("geolocation" in navigator) { - navigator.geolocation.getCurrentPosition( - (pos) => { - if (cancelled) return; - flyTo(pos.coords.latitude, pos.coords.longitude); - }, - () => { - if (cancelled) return; - fallbackIp(); - }, - { enableHighAccuracy: true, timeout: 8000 } - ); - } else { - fallbackIp(); - } - - return () => { - cancelled = true; - }; - }, [mapReady]); - - /* ------------ LOAD POSTS -------------- */ + /* ------------ POSITION UTILISATEUR (GPS/IP) -------------- */ useEffect(() => { const map = mapRef.current; if (!map) return; let cancelled = false; + function saveUserPos(lat, lon) { + if (cancelled) return; + const coords = [lon, lat]; + setUserPosition(coords); + try { + localStorage.setItem(LAST_POS_KEY, JSON.stringify({ lat, lon })); + } catch (_) {} + + // si on N'A PAS de lastView, on utilise le geo pour centrer + if (!hasLastView) { + map.flyTo({ + center: coords, + zoom: 9, + speed: 1.2, + curve: 1.5, + essential: true, + }); + } + } + + // 1) dernière position utilisateur (pas la vue) + try { + const raw = localStorage.getItem(LAST_POS_KEY); + if (raw) { + const p = JSON.parse(raw); + if (typeof p.lat === "number" && typeof p.lon === "number") { + setUserPosition([p.lon, p.lat]); + } + } + } catch (_) {} + + // 2) GPS → 3) IP backend + function viaIpFallback() { + (async () => { + const ipCoords = await fetchIpLocation(); + if (!ipCoords || cancelled) return; + const [lon, lat] = ipCoords; + saveUserPos(lat, lon); + })(); + } + + if ("geolocation" in navigator) { + navigator.geolocation.getCurrentPosition( + (pos) => { + if (cancelled) return; + saveUserPos(pos.coords.latitude, pos.coords.longitude); + }, + () => { + if (cancelled) return; + viaIpFallback(); + }, + { + enableHighAccuracy: true, + timeout: 8000, + maximumAge: 30000, + } + ); + } else { + viaIpFallback(); + } + + return () => { + cancelled = true; + }; + }, [hasLastView]); + + /* ------------ SOUS-CATEGORIES (bas) -------------- */ + const bottomCategories = + CATEGORY_MAP[mainFilter] || CATEGORY_MAP.Default; + + useEffect(() => { + if (!bottomCategories.length) return; + if (!subFilter || !bottomCategories.includes(subFilter)) { + setSubFilter(bottomCategories[0]); + } + }, [mainFilter, bottomCategories, subFilter]); + + /* ------------ LOAD POSTS POUR LA ZONE COURANTE + FILTRES -------------- */ + useEffect(() => { + const map = mapRef.current; + if (!map) return; + if (!viewParams.center) return; + + let cancelled = false; + async function load() { try { setStatus("Loading posts..."); - const raw = await fetchPosts(); - const posts = Array.isArray(raw) ? raw : raw?.items || []; + + const [lng, lat] = viewParams.center; + const radiusKm = viewParams.radiusKm || 250; + + const catCode = categoryCode(mainFilter); + const subCatParam = + subFilter && subFilter.toUpperCase() !== "ALL" ? subFilter : ""; + + const posts = await fetchPosts({ + category: catCode, + subCategory: subCatParam, + time: timeFilter, + lat, + lon: lng, + radiusKm, + }); if (cancelled) return; @@ -151,13 +318,20 @@ export default function MapView() { markersRef.current = []; expandedElRef.current = null; - const bounds = new maplibregl.LngLatBounds(); - let hasCoords = false; - posts.forEach((post) => { const coords = getCoords(post); if (!coords) return; + const [lngP, latP] = coords; + if ( + typeof lngP !== "number" || + typeof latP !== "number" || + Number.isNaN(lngP) || + Number.isNaN(latP) + ) { + return; + } + const { title, body, short } = extract(post); const compactHTML = ` @@ -173,7 +347,9 @@ export default function MapView() {
${title} -
${body}
+
+ ${body} +
`; @@ -181,7 +357,10 @@ export default function MapView() { el.className = "post-marker post-marker-compact"; el.innerHTML = compactHTML; - const marker = new maplibregl.Marker({ element: el, anchor: "bottom" }) + const marker = new maplibregl.Marker({ + element: el, + anchor: "bottom", + }) .setLngLat(coords) .addTo(map); @@ -215,15 +394,8 @@ export default function MapView() { e.preventDefault(); toggle(); }); - - bounds.extend(coords); - hasCoords = true; }); - if (hasCoords) { - map.fitBounds(bounds, { padding: 40, maxZoom: 13 }); - } - setStatus(posts.length ? "" : "No posts found."); } catch (err) { console.error("Erreur chargement posts:", err); @@ -236,9 +408,134 @@ export default function MapView() { return () => { cancelled = true; }; + }, [viewParams, mainFilter, timeFilter, subFilter, reloadToken]); + + /* ------------ WEBSOCKET : RELOAD SUR new_post -------------- */ + useEffect(() => { + const proto = window.location.protocol === "https:" ? "wss" : "ws"; + const host = + window.location.port === "5173" + ? `${window.location.hostname}:8081` + : window.location.host; + + const wsUrl = `${proto}://${host}/ws`; + let ws; + + try { + ws = new WebSocket(wsUrl); + } catch (e) { + console.warn("WebSocket init error:", e); + return; + } + + ws.onmessage = (ev) => { + try { + const msg = JSON.parse(ev.data); + if (msg.type === "new_post") { + setReloadToken((n) => n + 1); + } + } catch (_) {} + }; + + ws.onerror = (e) => { + console.warn("WebSocket error:", e); + }; + + return () => { + if (ws && ws.readyState === WebSocket.OPEN) { + ws.close(); + } + }; }, []); - const bottomCategories = CATEGORY_MAP[mainFilter] || CATEGORY_MAP.Default; + /* ------------ ACTIONS UI -------------- */ + + const handleFlyToMe = () => { + const map = mapRef.current; + if (!map || !userPosition) return; + map.flyTo({ + center: userPosition, + zoom: 9, + speed: 1.1, + curve: 1.5, + essential: true, + }); + }; + + const handlePickCenter = () => { + const map = mapRef.current; + if (!map) return; + const center = map.getCenter(); + setDraftCoords([center.lng, center.lat]); + }; + + const handleOpenCreate = () => { + setIsCreating(true); + setSaveError(""); + setDraftTitle(""); + setDraftBody(""); + + const main = mainFilter === "Default" ? "News" : mainFilter; + setDraftCategory(main); + const list = CATEGORY_MAP[main] || CATEGORY_MAP.Default; + setDraftSubCategory(list[0]); + setDraftCoords(null); + }; + + const handleSubmitPost = async () => { + if (isSaving) return; + setSaveError(""); + + const map = mapRef.current; + if (!map) return; + + let coords = draftCoords; + if (!coords) { + const center = map.getCenter(); + coords = [center.lng, center.lat]; + } + + if (!coords) { + setSaveError("Position inconnue. Déplace la carte et tap sur la cible."); + return; + } + + const [lng, lat] = coords; + + if (!draftTitle.trim()) { + setSaveError("Le titre est requis."); + return; + } + + const categoryUpper = + draftCategory && draftCategory.toUpperCase() !== "DEFAULT" + ? draftCategory.toUpperCase() + : "NEWS"; + + const payload = { + title: draftTitle.trim(), + snippet: draftBody.trim() || draftTitle.trim(), + category: categoryUpper, + sub_category: draftSubCategory || "ALL", + lat, + lon: lng, + author: "tommy", + }; + + try { + setIsSaving(true); + await createPost(payload); + setIsSaving(false); + setIsCreating(false); + setReloadToken((n) => n + 1); + } catch (e) { + console.error("createPost error:", e); + setIsSaving(false); + setSaveError("Erreur lors de la création du post."); + } + }; + + /* ------------ RENDER -------------- */ return (
@@ -254,14 +551,29 @@ export default function MapView() {
+ - {/* LEFT */} + {/* LEFT — temps */}
- - - - + {[ + ["NOW", "Now"], + ["TODAY", "Today"], + ["RECENT", "Recent"], + ["PAST", "Past"], + ].map(([code, label]) => ( + + ))}
{/* RIGHT — filtres principaux */} @@ -279,19 +591,119 @@ export default function MapView() { ))} - {/* BOTTOM — arc/orbite sous le globe */} + {/* BOTTOM — arc courbé inversé autour du globe */}
- {bottomCategories.map((c, idx) => ( + {bottomCategories.map((c) => ( ))}
+ + {/* BOUTON "MY SPOT" */} +
+ +
+ + {/* CROSSHAIR SUR LA MAP (au-dessus du popup) */} + {isCreating && ( +
+
+
+ )} + + {/* PANEL CREATION DE POST (1/5 du bas) */} + {isCreating && ( +
+
+ Create wire + +
+ +
+ + Déplace la carte sous la cible, puis tap sur la cible pour fixer + la position. + {draftCoords ? " ✓ Position choisie" : ""} + +
+ +
+ + + +
+ + setDraftTitle(e.target.value)} + /> + +