Auto deploy: 2025-12-09 01:46:49
This commit is contained in:
parent
222a907dd2
commit
1acb77f7d3
|
|
@ -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"
|
||||||
11
src/App.jsx
11
src/App.jsx
|
|
@ -1,23 +1,16 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import TopBar from "./components/Layout/TopBar";
|
import TopBar from "./components/Layout/TopBar";
|
||||||
import PageContainer from "./components/Layout/PageContainer";
|
|
||||||
import MapView from "./components/Map/MapView";
|
import MapView from "./components/Map/MapView";
|
||||||
import PostList from "./components/Posts/PostList";
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<div className="app-root">
|
<div className="app-root">
|
||||||
<TopBar />
|
<TopBar />
|
||||||
<PageContainer>
|
<div className="main-shell">
|
||||||
<div className="layout-main">
|
<div className="map-shell">
|
||||||
<div className="layout-left">
|
|
||||||
<PostList />
|
|
||||||
</div>
|
|
||||||
<div className="layout-right">
|
|
||||||
<MapView />
|
<MapView />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageContainer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,59 @@
|
||||||
import axios from "axios";
|
/**
|
||||||
|
* SocioWire API client
|
||||||
|
* Frontend -> Backend (port 8081 via proxy /api)
|
||||||
|
*/
|
||||||
|
|
||||||
const api = axios.create({
|
const API_BASE = "/api";
|
||||||
baseURL: "http://localhost:8080", // ton backend SocioWire
|
|
||||||
timeout: 10000,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
/** Récupère la liste des posts */
|
||||||
export async function fetchPosts() {
|
export async function fetchPosts() {
|
||||||
const res = await api.get("/posts");
|
const res = await fetch(`${API_BASE}/posts`, {
|
||||||
return res.data;
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@ import React from "react";
|
||||||
export default function TopBar() {
|
export default function TopBar() {
|
||||||
return (
|
return (
|
||||||
<header className="topbar">
|
<header className="topbar">
|
||||||
<div className="topbar-title">SocioWire</div>
|
<div className="logo-circle">SW</div>
|
||||||
<div className="topbar-actions">
|
<div className="title-block">
|
||||||
<button className="btn-primary">Reload posts</button>
|
<div className="main-title">SOCIOWIRE.com</div>
|
||||||
|
<div className="sub-title">Wired to life</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button className="btn-primary">Login</button>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,297 @@
|
||||||
import React, { useEffect, useRef } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import maplibregl from "maplibre-gl";
|
import maplibregl from "maplibre-gl";
|
||||||
import "maplibre-gl/dist/maplibre-gl.css";
|
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() {
|
export default function MapView() {
|
||||||
const mapContainerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const mapRef = 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(() => {
|
useEffect(() => {
|
||||||
if (mapRef.current) return;
|
if (!containerRef.current) return;
|
||||||
|
|
||||||
mapRef.current = new maplibregl.Map({
|
const map = new maplibregl.Map({
|
||||||
container: mapContainerRef.current,
|
container: containerRef.current,
|
||||||
style: "https://demotiles.maplibre.org/style.json",
|
style: "https://demotiles.maplibre.org/style.json",
|
||||||
center: [-73.5673, 45.5017], // Montréal par défaut
|
center: [-70, 47],
|
||||||
zoom: 11,
|
zoom: 5,
|
||||||
|
pitch: 45,
|
||||||
|
bearing: -20,
|
||||||
|
antialias: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
||||||
|
|
||||||
|
mapRef.current = map;
|
||||||
|
setMapReady(true);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (mapRef.current) {
|
markersRef.current.forEach((m) => m.marker.remove());
|
||||||
mapRef.current.remove();
|
markersRef.current = [];
|
||||||
|
map.remove();
|
||||||
mapRef.current = null;
|
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 = `
|
||||||
|
<div class="marker-inner">
|
||||||
|
<div class="marker-dot"></div>
|
||||||
|
<span class="marker-text">${short}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const expandedHTML = `
|
||||||
|
<div class="marker-expanded-inner">
|
||||||
|
<div class="marker-expanded-header">
|
||||||
|
<div class="marker-dot"></div>
|
||||||
|
<span class="marker-expanded-title">${title}</span>
|
||||||
|
</div>
|
||||||
|
<div class="marker-expanded-body">${body}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div className="map-view">
|
<div className="map-view">
|
||||||
<div ref={mapContainerRef} className="maplibre-container" />
|
<div ref={containerRef} className="map-container" />
|
||||||
|
{status && <div className="map-status">{status}</div>}
|
||||||
|
|
||||||
|
{/* TOP */}
|
||||||
|
<div className="map-overlay map-overlay-top">
|
||||||
|
<div className="lookat-box">Look at…</div>
|
||||||
|
<div className="wire-title">Place your wire</div>
|
||||||
|
<div className="zoom-buttons">
|
||||||
|
<button className="chip-hex">Zoom In</button>
|
||||||
|
<button className="chip-hex">Zoom Out</button>
|
||||||
|
<button className="chip-hex">Swall</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* LEFT */}
|
||||||
|
<div className="map-overlay map-overlay-left">
|
||||||
|
<button className="chip-round">Now</button>
|
||||||
|
<button className="chip-round">Today</button>
|
||||||
|
<button className="chip-round">Recent</button>
|
||||||
|
<button className="chip-round">Past</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* RIGHT — filtres principaux */}
|
||||||
|
<div className="map-overlay map-overlay-right">
|
||||||
|
{["News", "Friends", "Events", "Market"].map((cat) => (
|
||||||
|
<button
|
||||||
|
key={cat}
|
||||||
|
className={
|
||||||
|
mainFilter === cat ? "chip-pill chip-selected" : "chip-pill"
|
||||||
|
}
|
||||||
|
onClick={() => setMainFilter(cat)}
|
||||||
|
>
|
||||||
|
{cat}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* BOTTOM — arc/orbite sous le globe */}
|
||||||
|
<div className="map-overlay map-overlay-bottom">
|
||||||
|
{bottomCategories.map((c, idx) => (
|
||||||
|
<button
|
||||||
|
key={c}
|
||||||
|
className={
|
||||||
|
idx === 0 ? "chip-egg chip-egg-active" : "chip-egg"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{c}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,17 +10,27 @@ function getKmFromPost(post) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PostCard({ post }) {
|
export default function PostCard({ post, selected, onSelect }) {
|
||||||
const km = getKmFromPost(post);
|
|
||||||
const title = post.title || post.name || "Post";
|
const title = post.title || post.name || "Post";
|
||||||
|
|
||||||
|
const fullBody =
|
||||||
|
post.body || post.content || post.text || "";
|
||||||
|
|
||||||
|
// texte court dans le Sociowall
|
||||||
const body =
|
const body =
|
||||||
post.body ||
|
fullBody.length > 80 ? fullBody.slice(0, 77) + "..." : fullBody;
|
||||||
post.content ||
|
|
||||||
post.text ||
|
const km = getKmFromPost(post);
|
||||||
JSON.stringify(post, null, 2);
|
|
||||||
|
function handleClick() {
|
||||||
|
if (onSelect) onSelect(post);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="post-card">
|
<article
|
||||||
|
className={"post-card" + (selected ? " selected" : "")}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
<h3>{title}</h3>
|
<h3>{title}</h3>
|
||||||
<p>{body}</p>
|
<p>{body}</p>
|
||||||
{km != null && <span className="post-km">{km} km</span>}
|
{km != null && <span className="post-km">{km} km</span>}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useState, useMemo } from "react";
|
||||||
import PostCard from "./PostCard";
|
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) {
|
function getKmFromPost(post) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -17,57 +11,28 @@ function getKmFromPost(post) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PostList() {
|
export default function PostList({
|
||||||
const [posts, setPosts] = useState([]);
|
posts,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
selectedPost,
|
||||||
|
onSelectPost,
|
||||||
|
}) {
|
||||||
const [kmFilter, setKmFilter] = useState(1000);
|
const [kmFilter, setKmFilter] = useState(1000);
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
|
|
||||||
useEffect(() => {
|
const filtered = useMemo(
|
||||||
let cancelled = false;
|
() =>
|
||||||
|
posts.filter((p) => {
|
||||||
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);
|
const km = getKmFromPost(p);
|
||||||
if (km == null) return true; // si pas de distance, on garde
|
if (km == null) return true;
|
||||||
return km <= kmFilter;
|
return km <= kmFilter;
|
||||||
});
|
}),
|
||||||
|
[posts, kmFilter]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="post-list">
|
<div className="post-list">
|
||||||
<div className="post-list-header">
|
<div className="post-list-header">
|
||||||
<h2>Posts</h2>
|
|
||||||
<div className="km-filter">
|
<div className="km-filter">
|
||||||
<label>
|
<label>
|
||||||
Rayon : <span>{kmFilter} km</span>
|
Rayon : <span>{kmFilter} km</span>
|
||||||
|
|
@ -86,7 +51,12 @@ export default function PostList() {
|
||||||
{error && <p className="status-text status-error">{error}</p>}
|
{error && <p className="status-text status-error">{error}</p>}
|
||||||
|
|
||||||
{filtered.map((p) => (
|
{filtered.map((p) => (
|
||||||
<PostCard key={p.id ?? p._id} post={p} />
|
<PostCard
|
||||||
|
key={p.id ?? p._id}
|
||||||
|
post={p}
|
||||||
|
selected={selectedPost && (selectedPost.id ?? selectedPost._id) === (p.id ?? p._id)}
|
||||||
|
onSelect={onSelectPost}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{!loading && filtered.length === 0 && (
|
{!loading && filtered.length === 0 && (
|
||||||
|
|
|
||||||
11
src/main.jsx
11
src/main.jsx
|
|
@ -1,7 +1,14 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import App from "./App";
|
import App from "./App.jsx";
|
||||||
import "./styles/global.css";
|
|
||||||
|
import "./styles/base.css";
|
||||||
|
import "./styles/layout.css";
|
||||||
|
import "./styles/topbar.css";
|
||||||
|
import "./styles/map.css";
|
||||||
|
import "./styles/markers.css";
|
||||||
|
import "./styles/popup.css";
|
||||||
|
import "./styles/overlays.css";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
/* ========== BASE / RESET ========== */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background: #050816;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
|
@ -1,164 +1,284 @@
|
||||||
*,
|
/* RESET */
|
||||||
*::before,
|
* { box-sizing: border-box; }
|
||||||
*::after {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
||||||
sans-serif;
|
|
||||||
background: #050816;
|
background: #050816;
|
||||||
color: #f5f5f5;
|
color: #fff;
|
||||||
}
|
font-family: system-ui, sans-serif;
|
||||||
|
|
||||||
.app-root {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* TOPBAR */
|
||||||
.topbar {
|
.topbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 0.75rem 1rem;
|
gap: 0.6rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
background: #0b1020;
|
background: #0b1020;
|
||||||
border-bottom: 1px solid #1f2933;
|
border-bottom: 1px solid #1f2937;
|
||||||
}
|
}
|
||||||
|
.logo-circle {
|
||||||
.topbar-title {
|
width: 32px;
|
||||||
font-size: 1.1rem;
|
height: 32px;
|
||||||
font-weight: 600;
|
background: radial-gradient(circle, #38bdf8, #1d4ed8, #020617);
|
||||||
}
|
border-radius: 50%;
|
||||||
|
|
||||||
.topbar-actions {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
.title-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.main-title { font-size: 1rem; font-weight: 700; }
|
||||||
|
.sub-title { font-size: 0.75rem; opacity: 0.7; }
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
padding: 0.35rem 0.8rem;
|
padding: 0.25rem 0.7rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
border: 1px solid #2563eb;
|
border: 1px solid #22c55e;
|
||||||
background: #1d4ed8;
|
background: #16a34a;
|
||||||
color: white;
|
color: #f9fafb;
|
||||||
font-size: 0.85rem;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
/* ROOT & LAYOUT */
|
||||||
opacity: 0.9;
|
.app-root {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
.main-shell {
|
||||||
.page-container {
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-main {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(280px, 380px) minmax(0, 1fr);
|
|
||||||
gap: 0.75rem;
|
|
||||||
height: calc(100vh - 56px - 1.5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-left {
|
|
||||||
background: #0b1020;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
padding: 0.75rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout-right {
|
|
||||||
background: #020617;
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
padding: 0.75rem;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Post list + slider */
|
|
||||||
.post-list {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-list-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 0.75rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-list h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.km-filter {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
gap: 0.15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.km-filter label {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.km-filter span {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.km-filter input[type="range"] {
|
|
||||||
width: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-card {
|
|
||||||
border-radius: 0.6rem;
|
|
||||||
padding: 0.6rem;
|
padding: 0.6rem;
|
||||||
background: #111827;
|
}
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
|
/* MAP FULLSCREEN */
|
||||||
|
.map-shell {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
border: 1px solid #1f2937;
|
border: 1px solid #1f2937;
|
||||||
|
background: #020617;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post-card h3 {
|
|
||||||
margin: 0 0 0.25rem 0;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-card p {
|
|
||||||
margin: 0 0 0.4rem 0;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-km {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-text {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
opacity: 0.85;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-error {
|
|
||||||
color: #f97373;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* MapLibre */
|
|
||||||
.map-view {
|
.map-view {
|
||||||
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 0.75rem;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid #1f2937;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.maplibre-container {
|
.maplibre-container {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Overlays autour de la map */
|
||||||
|
.map-overlay {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.map-overlay * {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Haut */
|
||||||
|
.top-bar {
|
||||||
|
top: 0.4rem;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0 0.6rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.lookat-box {
|
||||||
|
padding: 0.25rem 0.6rem;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
.wire-title { font-size: 0.7rem; opacity: 0.7; }
|
||||||
|
.zoom-buttons { display: flex; gap: 0.3rem; }
|
||||||
|
|
||||||
|
/* Gauche */
|
||||||
|
.left-bar {
|
||||||
|
left: 0.4rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Droite */
|
||||||
|
.right-bar {
|
||||||
|
right: 0.4rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bas */
|
||||||
|
.bottom-bar {
|
||||||
|
bottom: 0.4rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chips */
|
||||||
|
.chip-round,
|
||||||
|
.chip-pill,
|
||||||
|
.chip-egg,
|
||||||
|
.chip-hex {
|
||||||
|
background: rgba(20,25,50,0.9);
|
||||||
|
border: 1px solid #1d4ed8;
|
||||||
|
padding: 0.35rem 0.6rem;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 999px;
|
||||||
|
box-shadow: 0 4px 10px rgba(0,0,0,0.6);
|
||||||
|
}
|
||||||
|
.chip-egg { border-radius: 999px; }
|
||||||
|
.chip-hex { border-radius: 0.4rem; }
|
||||||
|
.chip-pill { border-radius: 999px; }
|
||||||
|
|
||||||
|
/* Status en bas à gauche */
|
||||||
|
.map-status {
|
||||||
|
position: absolute;
|
||||||
|
left: 0.5rem;
|
||||||
|
bottom: 0.5rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
background: rgba(0,0,0,0.6);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === MARKERS (petits posts) === */
|
||||||
|
.post-marker {
|
||||||
|
position: relative;
|
||||||
|
background: rgba(15, 23, 42, 0.96);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.9);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.7);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.post-marker::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: -7px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-width: 7px 7px 0 7px;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: rgba(15, 23, 42, 0.96) transparent transparent transparent;
|
||||||
|
}
|
||||||
|
.post-marker-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.post-marker-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 140px;
|
||||||
|
}
|
||||||
|
.post-marker-title {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.post-marker-snippet {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.post-marker-image {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.8);
|
||||||
|
}
|
||||||
|
.post-marker-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.post-marker-image-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: radial-gradient(circle at 30% 30%, #38bdf8, #1d4ed8, #020617);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === POPUP GROS POST === */
|
||||||
|
.maplibregl-popup-content {
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.popup-post-card {
|
||||||
|
background: rgba(15, 23, 42, 0.97);
|
||||||
|
border-radius: 1rem;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.9);
|
||||||
|
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.85);
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
.popup-post-inner {
|
||||||
|
padding: 0.65rem 0.75rem 0.75rem 0.75rem;
|
||||||
|
}
|
||||||
|
.popup-post-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.6rem;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
.popup-post-image {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.9);
|
||||||
|
}
|
||||||
|
.popup-post-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.popup-post-image-placeholder {
|
||||||
|
background: radial-gradient(circle at 30% 30%, #38bdf8, #1d4ed8, #020617);
|
||||||
|
}
|
||||||
|
.popup-post-title {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.popup-post-body {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.35;
|
||||||
|
color: #cbd5f5;
|
||||||
|
max-height: 190px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/* ========== LAYOUT GLOBAL (structure) ========== */
|
||||||
|
|
||||||
|
.app-root {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-shell {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
padding: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Conteneur de la carte plein écran */
|
||||||
|
.map-shell {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
border-radius: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #1f2937;
|
||||||
|
background: #020617;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
/* ========== CARTE / CONTAINER MAPLIBRE ========== */
|
||||||
|
|
||||||
|
.map-view {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-container,
|
||||||
|
.maplibre-container {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Petit statut en bas à gauche (ex: "Loading posts...") */
|
||||||
|
.map-status {
|
||||||
|
position: absolute;
|
||||||
|
left: 0.5rem;
|
||||||
|
bottom: 0.5rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
/* ========== MARKERS (PETITS & GROS POSTS SUR LA MAP) ========== */
|
||||||
|
|
||||||
|
/* base commune */
|
||||||
|
.post-marker {
|
||||||
|
position: relative;
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 12px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: stretch;
|
||||||
|
cursor: pointer;
|
||||||
|
transform: translateY(0); /* important pour garder le même ancrage */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- PETIT POST (COMPACT) --- */
|
||||||
|
.post-marker-compact {
|
||||||
|
background: rgba(0, 0, 0, 0.78);
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.35);
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgba(15, 23, 42, 0.7),
|
||||||
|
0 6px 18px rgba(0, 0, 0, 0.8),
|
||||||
|
0 0 16px rgba(56, 189, 248, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-marker-compact::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: -7px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 7px 7px 0 7px;
|
||||||
|
border-color: rgba(0, 0, 0, 0.78) transparent transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-dot {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle at 30% 30%, #4ade80, #22c55e, #0284c7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-text {
|
||||||
|
max-width: 140px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- GROS POST (EXPANDED) --- */
|
||||||
|
.post-marker-expanded {
|
||||||
|
background: rgba(15, 23, 42, 0.98);
|
||||||
|
padding: 8px 10px 10px 10px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.9);
|
||||||
|
box-shadow:
|
||||||
|
0 10px 30px rgba(0, 0, 0, 0.9),
|
||||||
|
0 0 24px rgba(56, 189, 248, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-marker-expanded::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: -10px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 10px 10px 0 10px;
|
||||||
|
border-color: rgba(15, 23, 42, 0.98) transparent transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* contenu du gros post */
|
||||||
|
.marker-expanded-inner {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
max-width: 230px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-expanded-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-expanded-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-expanded-body {
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
opacity: 0.95;
|
||||||
|
max-height: 120px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
/* ==========================================
|
||||||
|
SOCIOWIRE — OVERLAY + ARC AUTOUR DU GLOBE
|
||||||
|
========================================== */
|
||||||
|
|
||||||
|
.map-overlay {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-overlay * {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------- TOP --------- */
|
||||||
|
|
||||||
|
.map-overlay-top {
|
||||||
|
top: 0.7rem;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 0.6rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lookat-box {
|
||||||
|
padding: 0.35rem 0.9rem;
|
||||||
|
background: rgba(0, 10, 30, 0.85);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.5);
|
||||||
|
color: #dbeafe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wire-title {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
color: #93c5fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------- LEFT --------- */
|
||||||
|
|
||||||
|
.map-overlay-left {
|
||||||
|
left: 0.7rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------- RIGHT --------- */
|
||||||
|
|
||||||
|
.map-overlay-right {
|
||||||
|
right: 0.7rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bouton principal sélectionné (News/Friends/Events/Market) */
|
||||||
|
.chip-selected {
|
||||||
|
background: linear-gradient(135deg, #2563eb, #0ea5e9);
|
||||||
|
border-color: #7dd3fc;
|
||||||
|
box-shadow: 0 0 20px rgba(56, 189, 248, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------- BOTTOM — ARC (type orbite) --------- */
|
||||||
|
|
||||||
|
.map-overlay-bottom {
|
||||||
|
bottom: 3.2rem; /* au-dessus de la barre Android */
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On crée un arc en jouant avec translateY + rotate */
|
||||||
|
.map-overlay-bottom .chip-egg:nth-child(1) {
|
||||||
|
transform: translateY(8px) rotate(-12deg);
|
||||||
|
}
|
||||||
|
.map-overlay-bottom .chip-egg:nth-child(2) {
|
||||||
|
transform: translateY(4px) rotate(-6deg);
|
||||||
|
}
|
||||||
|
.map-overlay-bottom .chip-egg:nth-child(3) {
|
||||||
|
transform: translateY(0px) rotate(0deg);
|
||||||
|
}
|
||||||
|
.map-overlay-bottom .chip-egg:nth-child(4) {
|
||||||
|
transform: translateY(4px) rotate(6deg);
|
||||||
|
}
|
||||||
|
.map-overlay-bottom .chip-egg:nth-child(5) {
|
||||||
|
transform: translateY(8px) rotate(12deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------- CHIPS (boutons) --------- */
|
||||||
|
|
||||||
|
.chip-round,
|
||||||
|
.chip-pill,
|
||||||
|
.chip-egg,
|
||||||
|
.chip-hex {
|
||||||
|
background: rgba(15, 23, 42, 0.96);
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.85);
|
||||||
|
color: #e5e7eb;
|
||||||
|
padding: 0.45rem 0.9rem;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
0 5px 12px rgba(0, 0, 0, 0.7),
|
||||||
|
0 0 12px rgba(56, 189, 248, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* boutons d’orbite (catégories dynamiques) */
|
||||||
|
.chip-egg {
|
||||||
|
background: rgba(0, 10, 30, 0.9);
|
||||||
|
border: 1px solid rgba(56, 189, 248, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* catégorie sélectionnée (première) */
|
||||||
|
.chip-egg-active {
|
||||||
|
background: linear-gradient(135deg, #1d4ed8, #0ea5e9);
|
||||||
|
border-color: #7dd3fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* boutons hex (Zoom, Swall) */
|
||||||
|
.chip-hex {
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
/* ========== POPUP (GROS POST AU CLIC) ========== */
|
||||||
|
|
||||||
|
.maplibregl-popup-content {
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: visible;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* conteneur du gros post */
|
||||||
|
.popup-post-card {
|
||||||
|
background: rgba(15, 23, 42, 0.98);
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.9);
|
||||||
|
box-shadow:
|
||||||
|
0 10px 30px rgba(0, 0, 0, 0.9),
|
||||||
|
0 0 26px rgba(56, 189, 248, 0.55);
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* crochet du gros post */
|
||||||
|
.popup-post-card::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: -10px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 10px 10px 0 10px;
|
||||||
|
border-color: rgba(15, 23, 42, 0.98) transparent transparent transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-post-inner {
|
||||||
|
padding: 8px 10px 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-post-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* petit rond bleu dans le gros post, pour garder le lien visuel */
|
||||||
|
.popup-post-dot {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: radial-gradient(circle at 30% 30%, #4ade80, #22c55e, #0284c7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-post-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-post-body {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.35;
|
||||||
|
max-height: 180px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-post-empty {
|
||||||
|
opacity: 0.8;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/* ========== TOP BAR (SOCIOWIRE header) ========== */
|
||||||
|
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 0.6rem;
|
||||||
|
padding: 0.4rem 0.8rem;
|
||||||
|
background: #0b1020;
|
||||||
|
border-bottom: 1px solid #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-circle {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: radial-gradient(circle, #38bdf8, #1d4ed8, #020617);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-title {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
padding: 0.25rem 0.7rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid #22c55e;
|
||||||
|
background: #16a34a;
|
||||||
|
color: #f9fafb;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,17 @@
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from "vite";
|
||||||
import react from '@vitejs/plugin-react'
|
import react from "@vitejs/plugin-react";
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
})
|
server: {
|
||||||
|
host: "0.0.0.0",
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: "http://localhost:8081", // ton backend
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue