-
+
SW
+
+
SOCIOWIRE.com
+
Wired to life
+
);
}
diff --git a/src/components/Map/MapView.jsx b/src/components/Map/MapView.jsx
index 3a8fe2a..5526b1d 100644
--- a/src/components/Map/MapView.jsx
+++ b/src/components/Map/MapView.jsx
@@ -1,32 +1,297 @@
-import React, { useEffect, useRef } from "react";
+import React, { useEffect, useRef, useState } from "react";
import maplibregl from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
+import { fetchPosts } from "../../api/client";
+
+/* ------------ UTILS -------------- */
+function getCoords(post) {
+ const lat =
+ post.lat ??
+ post.latitude ??
+ post.location?.lat ??
+ (Array.isArray(post.coords) ? post.coords[1] : null);
+
+ const lon =
+ post.lon ??
+ post.lng ??
+ post.location?.lng ??
+ (Array.isArray(post.coords) ? post.coords[0] : null);
+
+ if (typeof lat === "number" && typeof lon === "number") return [lon, lat];
+ return null;
+}
+
+function extract(post) {
+ const title = post.title || post.text || post.body || "Post";
+ const body =
+ 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 -------------- */
+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"],
+ Default: ["World", "Actu", "Tech", "Finan", "Sport"],
+};
export default function MapView() {
- const mapContainerRef = useRef(null);
+ const containerRef = useRef(null);
const mapRef = useRef(null);
+ const markersRef = useRef([]); // { marker, el, compactHTML, expandedHTML }
+ const expandedElRef = useRef(null);
+ const [status, setStatus] = useState("Loading posts...");
+ const [mapReady, setMapReady] = useState(false);
+ const [mainFilter, setMainFilter] = useState("Default");
+
+ /* ------------ INIT MAP -------------- */
useEffect(() => {
- if (mapRef.current) return;
+ if (!containerRef.current) return;
- mapRef.current = new maplibregl.Map({
- container: mapContainerRef.current,
+ const map = new maplibregl.Map({
+ container: containerRef.current,
style: "https://demotiles.maplibre.org/style.json",
- center: [-73.5673, 45.5017], // Montréal par défaut
- zoom: 11,
+ center: [-70, 47],
+ zoom: 5,
+ pitch: 45,
+ bearing: -20,
+ antialias: true,
});
+ map.addControl(new maplibregl.NavigationControl(), "top-right");
+
+ mapRef.current = map;
+ setMapReady(true);
+
return () => {
- if (mapRef.current) {
- mapRef.current.remove();
- mapRef.current = null;
- }
+ markersRef.current.forEach((m) => m.marker.remove());
+ markersRef.current = [];
+ map.remove();
+ mapRef.current = null;
};
}, []);
+ /* ------------ 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 -------------- */
+ useEffect(() => {
+ const map = mapRef.current;
+ if (!map) return;
+
+ let cancelled = false;
+
+ async function load() {
+ try {
+ setStatus("Loading posts...");
+ const raw = await fetchPosts();
+ const posts = Array.isArray(raw) ? raw : raw?.items || [];
+
+ if (cancelled) return;
+
+ markersRef.current.forEach((m) => m.marker.remove());
+ markersRef.current = [];
+ expandedElRef.current = null;
+
+ const bounds = new maplibregl.LngLatBounds();
+ let hasCoords = false;
+
+ posts.forEach((post) => {
+ const coords = getCoords(post);
+ if (!coords) return;
+
+ const { title, body, short } = extract(post);
+
+ const compactHTML = `
+
+ `;
+
+ const expandedHTML = `
+
+ `;
+
+ const el = document.createElement("div");
+ el.className = "post-marker post-marker-compact";
+ el.innerHTML = compactHTML;
+
+ const marker = new maplibregl.Marker({ element: el, anchor: "bottom" })
+ .setLngLat(coords)
+ .addTo(map);
+
+ const meta = { marker, el, compactHTML, expandedHTML };
+ markersRef.current.push(meta);
+
+ const toggle = () => {
+ if (expandedElRef.current && expandedElRef.current !== el) {
+ const prev = markersRef.current.find(
+ (m) => m.el === expandedElRef.current
+ );
+ if (prev) {
+ prev.el.className = "post-marker post-marker-compact";
+ prev.el.innerHTML = prev.compactHTML;
+ }
+ }
+
+ if (expandedElRef.current === el) {
+ el.className = "post-marker post-marker-compact";
+ el.innerHTML = compactHTML;
+ expandedElRef.current = null;
+ } else {
+ el.className = "post-marker post-marker-expanded";
+ el.innerHTML = expandedHTML;
+ expandedElRef.current = el;
+ }
+ };
+
+ el.addEventListener("click", toggle);
+ el.addEventListener("touchend", (e) => {
+ 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);
+ if (!cancelled) setStatus("Error loading posts.");
+ }
+ }
+
+ load();
+
+ return () => {
+ cancelled = true;
+ };
+ }, []);
+
+ const bottomCategories = CATEGORY_MAP[mainFilter] || CATEGORY_MAP.Default;
+
return (
-
+
+ {status &&
{status}
}
+
+ {/* TOP */}
+
+
Look at…
+
Place your wire
+
+
+
+
+
+
+
+ {/* LEFT */}
+
+
+
+
+
+
+
+ {/* RIGHT — filtres principaux */}
+
+ {["News", "Friends", "Events", "Market"].map((cat) => (
+
+ ))}
+
+
+ {/* BOTTOM — arc/orbite sous le globe */}
+
+ {bottomCategories.map((c, idx) => (
+
+ ))}
+
);
}
diff --git a/src/components/Posts/PostCard.jsx b/src/components/Posts/PostCard.jsx
index 622df7a..52ca087 100644
--- a/src/components/Posts/PostCard.jsx
+++ b/src/components/Posts/PostCard.jsx
@@ -10,17 +10,27 @@ function getKmFromPost(post) {
);
}
-export default function PostCard({ post }) {
- const km = getKmFromPost(post);
+export default function PostCard({ post, selected, onSelect }) {
const title = post.title || post.name || "Post";
+
+ const fullBody =
+ post.body || post.content || post.text || "";
+
+ // texte court dans le Sociowall
const body =
- post.body ||
- post.content ||
- post.text ||
- JSON.stringify(post, null, 2);
+ fullBody.length > 80 ? fullBody.slice(0, 77) + "..." : fullBody;
+
+ const km = getKmFromPost(post);
+
+ function handleClick() {
+ if (onSelect) onSelect(post);
+ }
return (
-
+
{title}
{body}
{km != null && {km} km}
diff --git a/src/components/Posts/PostList.jsx b/src/components/Posts/PostList.jsx
index 6ea12c2..8091acf 100644
--- a/src/components/Posts/PostList.jsx
+++ b/src/components/Posts/PostList.jsx
@@ -1,11 +1,5 @@
-import React, { useEffect, useState } from "react";
+import React, { useState, useMemo } from "react";
import PostCard from "./PostCard";
-import { fetchPosts } from "../../api/client";
-
-const FALLBACK_POSTS = [
- { id: 1, title: "Exemple local", body: "Backend non joignable, posts mock.", km: 2 },
- { id: 2, title: "Deuxième post", body: "On repart en neuf propre.", km: 10 },
-];
function getKmFromPost(post) {
return (
@@ -17,57 +11,28 @@ function getKmFromPost(post) {
);
}
-export default function PostList() {
- const [posts, setPosts] = useState([]);
+export default function PostList({
+ posts,
+ loading,
+ error,
+ selectedPost,
+ onSelectPost,
+}) {
const [kmFilter, setKmFilter] = useState(1000);
- const [loading, setLoading] = useState(true);
- const [error, setError] = useState("");
- useEffect(() => {
- let cancelled = false;
-
- async function load() {
- try {
- setLoading(true);
- setError("");
- const data = await fetchPosts();
- if (!cancelled) {
- if (Array.isArray(data)) {
- setPosts(data);
- } else if (Array.isArray(data?.items)) {
- setPosts(data.items);
- } else {
- setPosts(FALLBACK_POSTS);
- }
- }
- } catch (err) {
- console.error("Erreur fetchPosts:", err);
- if (!cancelled) {
- setError("Impossible de charger les posts (backend).");
- setPosts(FALLBACK_POSTS);
- }
- } finally {
- if (!cancelled) setLoading(false);
- }
- }
-
- load();
-
- return () => {
- cancelled = true;
- };
- }, []);
-
- const filtered = posts.filter((p) => {
- const km = getKmFromPost(p);
- if (km == null) return true; // si pas de distance, on garde
- return km <= kmFilter;
- });
+ const filtered = useMemo(
+ () =>
+ posts.filter((p) => {
+ const km = getKmFromPost(p);
+ if (km == null) return true;
+ return km <= kmFilter;
+ }),
+ [posts, kmFilter]
+ );
return (
-
Posts