Auto deploy: 2025-12-09 01:46:49

This commit is contained in:
Your Name 2025-12-09 01:46:55 +00:00
parent 222a907dd2
commit 1acb77f7d3
17 changed files with 1087 additions and 229 deletions

14
fix.sh Normal file
View File

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

View File

@ -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>
); );
} }

View File

@ -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;
}

View File

@ -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>
); );
} }

View File

@ -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>
); );
} }

View File

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

View File

@ -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 && (

View File

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

12
src/styles/base.css Normal file
View File

@ -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;
}

View File

@ -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;
}

23
src/styles/layout.css Normal file
View File

@ -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;
}

26
src/styles/map.css Normal file
View File

@ -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;
}

108
src/styles/markers.css Normal file
View File

@ -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;
}

135
src/styles/overlays.css Normal file
View File

@ -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 dorbite (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;
}

70
src/styles/popup.css Normal file
View File

@ -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;
}

47
src/styles/topbar.css Normal file
View File

@ -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;
}

View File

@ -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,
},
},
},
});