diff --git a/fix.sh b/fix.sh new file mode 100644 index 0000000..3a2e23e --- /dev/null +++ b/fix.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +FILE="src/components/Map/MapView.jsx" + +echo "🛠 Fix MapLibre CSS import in $FILE" + +# Remplace l'ancien mauvais import par le bon +sed -i 's|import "maplibregl/dist/maplibre-gl.css";|import "maplibre-gl/dist/maplibre-gl.css";|g' "$FILE" + +# Vérifie le résultat +echo "🔍 Vérification de l'import..." +grep 'maplibre-gl' "$FILE" || echo "⚠️ Aucun import trouvé !" + +echo "✅ Correction terminée ! Maintenant lance : npm run dev" diff --git a/src/App.jsx b/src/App.jsx index 8ff4033..6834635 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,23 +1,16 @@ import React from "react"; import TopBar from "./components/Layout/TopBar"; -import PageContainer from "./components/Layout/PageContainer"; import MapView from "./components/Map/MapView"; -import PostList from "./components/Posts/PostList"; export default function App() { return (
- -
-
- -
-
- -
+
+
+
- +
); } diff --git a/src/api/client.js b/src/api/client.js index cf7b21a..ec901b9 100644 --- a/src/api/client.js +++ b/src/api/client.js @@ -1,13 +1,59 @@ -import axios from "axios"; +/** + * SocioWire API client + * Frontend -> Backend (port 8081 via proxy /api) + */ -const api = axios.create({ - baseURL: "http://localhost:8080", // ton backend SocioWire - timeout: 10000, -}); +const API_BASE = "/api"; +/** Récupère la liste des posts */ export async function fetchPosts() { - const res = await api.get("/posts"); - return res.data; + const res = await fetch(`${API_BASE}/posts`, { + credentials: "include", + }); + + if (!res.ok) { + console.error("fetchPosts failed", res.status); + throw new Error("Failed to load posts"); + } + + const data = await res.json(); + return Array.isArray(data) ? data : data.items || []; } -export default api; +/** + * Récupère la position approx de l'utilisateur. + * Le backend doit renvoyer { lat, lon } ou { latitude, longitude } etc. + */ +export async function fetchUserLocation() { + const res = await fetch(`${API_BASE}/location`, { + credentials: "include", + }); + + if (!res.ok) { + console.warn("fetchUserLocation failed", res.status); + return null; + } + + 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; +} diff --git a/src/components/Layout/TopBar.jsx b/src/components/Layout/TopBar.jsx index 50cf7a4..1c021d6 100644 --- a/src/components/Layout/TopBar.jsx +++ b/src/components/Layout/TopBar.jsx @@ -3,10 +3,12 @@ import React from "react"; export default function TopBar() { return (
-
SocioWire
-
- +
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 = ` +
+
+ ${short} +
+ `; + + const expandedHTML = ` +
+
+
+ ${title} +
+
${body}
+
+ `; + + 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