Auto deploy: 2025-12-09 03:18:01

This commit is contained in:
Your Name 2025-12-09 03:18:04 +00:00
parent 1acb77f7d3
commit 174f3784e3
4 changed files with 764 additions and 138 deletions

19
fix-maplibre-css.sh Executable file
View File

@ -0,0 +1,19 @@
#!/bin/bash
echo "Checking for maplibre-gl.css..."
if [ -f node_modules/maplibre-gl/dist/maplibre-gl.css ]; then
CSS="maplibre-gl/dist/maplibre-gl.css"
elif [ -f node_modules/maplibre-gl/maplibre-gl.css ]; then
CSS="maplibre-gl/maplibre-gl.css"
else
echo "❌ Aucun fichier CSS trouvé dans maplibre-gl"
exit 1
fi
echo "✔ CSS found at: $CSS"
echo "✔ Updating MapView.jsx..."
sed -i "s|import .*maplibre.*css.*|import \"$CSS\";|" src/components/Map/MapView.jsx
echo "✔ Done!"

View File

@ -1,13 +1,36 @@
/**
* SocioWire API client
* Frontend -> Backend (port 8081 via proxy /api)
* SocioWire frontend API client
*/
const API_BASE = "/api";
/** Récupère la liste des posts */
export async function fetchPosts() {
const res = await fetch(`${API_BASE}/posts`, {
/**
* Récupère la liste des posts avec filtres.
* filters: {
* category?: "NEWS" | "FRIENDS" | "EVENTS" | "MARKET"
* subCategory?: string
* time?: "NOW" | "TODAY" | "RECENT" | "PAST"
* lat?: number
* lon?: number
* radiusKm?: number
* }
*/
export async function fetchPosts(filters = {}) {
const params = new URLSearchParams();
if (filters.category) params.set("category", filters.category);
if (filters.subCategory) params.set("sub_category", filters.subCategory);
if (filters.time) params.set("time", filters.time);
if (typeof filters.lat === "number") params.set("lat", String(filters.lat));
if (typeof filters.lon === "number") params.set("lon", String(filters.lon));
if (typeof filters.radiusKm === "number") {
params.set("radius_km", String(filters.radiusKm));
}
const qs = params.toString();
const url = qs ? `${API_BASE}/posts?${qs}` : `${API_BASE}/posts`;
const res = await fetch(url, {
credentials: "include",
});
@ -21,39 +44,72 @@ export async function fetchPosts() {
}
/**
* Récupère la position approx de l'utilisateur.
* Le backend doit renvoyer { lat, lon } ou { latitude, longitude } etc.
* Récupère la position approx de l'utilisateur depuis le backend.
* Backend: GET /api/ip-location { lat: number, lon: number } ou "null"
*/
export async function fetchUserLocation() {
const res = await fetch(`${API_BASE}/location`, {
export async function fetchIpLocation() {
try {
const res = await fetch(`${API_BASE}/ip-location`, {
credentials: "include",
});
if (!res.ok) {
console.warn("fetchUserLocation failed", res.status);
console.warn("fetchIpLocation: HTTP", 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
const text = await res.text();
if (!text || text === "null") {
return null;
}
console.warn("fetchUserLocation: invalid payload", data);
let data;
try {
data = JSON.parse(text);
} catch (e) {
console.warn("fetchIpLocation: JSON parse error", e, text);
return null;
}
const lat = data.lat;
const lon = data.lon;
if (
typeof lat === "number" &&
typeof lon === "number" &&
!Number.isNaN(lat) &&
!Number.isNaN(lon)
) {
return [lon, lat]; // [lng, lat]
}
console.warn("fetchIpLocation: invalid payload", data);
return null;
} catch (err) {
console.warn("fetchIpLocation: network error", err);
return null;
}
}
/**
* Crée un nouveau post
* Backend: POST /api/post (postCreateHandler)
*/
export async function createPost(payload) {
const res = await fetch(`${API_BASE}/post`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
body: JSON.stringify(payload),
});
if (!res.ok) {
const text = await res.text().catch(() => "");
console.error("createPost failed", res.status, text);
throw new Error("Failed to create post");
}
return res.json().catch(() => ({}));
}

View File

@ -1,9 +1,44 @@
import React, { useEffect, useRef, useState } from "react";
import maplibregl from "maplibre-gl";
import "maplibre-gl/dist/maplibre-gl.css";
import { fetchPosts } from "../../api/client";
import { fetchPosts, fetchIpLocation, createPost } from "../../api/client";
/* ------------ UTILS -------------- */
const LAST_POS_KEY = "sociowire:lastPosition"; // position utilisateur (GPS/IP)
const LAST_VIEW_KEY = "sociowire:lastView"; // dernière vue carte (centre+zoom)
// petite haversine pour estimer le rayon en km en fonction du zoom
function haversineKm(lat1, lon1, lat2, lon2) {
const R = 6371;
const toRad = (deg) => (deg * Math.PI) / 180;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(toRad(lat1)) *
Math.cos(toRad(lat2)) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
// centre + rayon (km) de la vue actuelle
function getViewFromMap(map) {
const bounds = map.getBounds();
const center = bounds.getCenter();
const ne = bounds.getNorthEast();
let radiusKm = haversineKm(center.lat, center.lng, ne.lat, ne.lng);
if (!Number.isFinite(radiusKm) || radiusKm <= 0) {
radiusKm = 250;
}
radiusKm = Math.min(Math.max(radiusKm, 10), 1000); // clamp 101000 km
return {
center: [center.lng, center.lat],
radiusKm,
};
}
function getCoords(post) {
const lat =
post.lat ??
@ -24,12 +59,32 @@ function getCoords(post) {
function extract(post) {
const title = post.title || post.text || post.body || "Post";
const body =
post.body || post.content || post.text || "No additional details.";
post.snippet ||
post.body ||
post.content ||
post.text ||
"No additional details.";
const short = title.length > 28 ? title.slice(0, 25) + "..." : title;
return { title, body, short };
}
/* ------------ BOTTOM CATEGORIES -------------- */
// map bouton -> code backend
function categoryCode(mainFilter) {
switch (mainFilter) {
case "News":
return "NEWS";
case "Friends":
return "FRIENDS";
case "Events":
return "EVENT"; // backend: EVENT (singulier)
case "Market":
return "MARKET";
default:
return "";
}
}
/* ------------ CATEGORIES -------------- */
const CATEGORY_MAP = {
News: ["World", "Politics", "Tech", "Finance", "Local"],
Friends: ["Nearby", "Chats", "Groups", "Stories", "Favs"],
@ -41,12 +96,36 @@ const CATEGORY_MAP = {
export default function MapView() {
const containerRef = useRef(null);
const mapRef = useRef(null);
const markersRef = useRef([]); // { marker, el, compactHTML, expandedHTML }
const markersRef = useRef([]);
const expandedElRef = useRef(null);
const [status, setStatus] = useState("Loading posts...");
const [mapReady, setMapReady] = useState(false);
const [mainFilter, setMainFilter] = useState("Default");
const [mainFilter, setMainFilter] = useState("Default"); // News/Friends/Events/Market
const [timeFilter, setTimeFilter] = useState("RECENT"); // NOW/TODAY/RECENT/PAST
const [subFilter, setSubFilter] = useState(null); // sous-catégorie du bas
const [userPosition, setUserPosition] = useState(null); // [lng, lat]
const [reloadToken, setReloadToken] = useState(0);
const [viewParams, setViewParams] = useState({
center: null,
radiusKm: 250,
});
const [hasLastView, setHasLastView] = useState(false);
// création de post
const [isCreating, setIsCreating] = useState(false);
const [draftTitle, setDraftTitle] = useState("");
const [draftBody, setDraftBody] = useState("");
const [draftCategory, setDraftCategory] = useState("News");
const [draftSubCategory, setDraftSubCategory] = useState(
CATEGORY_MAP.News[0]
);
const [draftCoords, setDraftCoords] = useState(null); // [lng, lat]
const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState("");
/* ------------ INIT MAP -------------- */
useEffect(() => {
@ -55,17 +134,63 @@ export default function MapView() {
const map = new maplibregl.Map({
container: containerRef.current,
style: "https://demotiles.maplibre.org/style.json",
center: [-70, 47],
zoom: 5,
center: [-95, 40],
zoom: 3.5,
pitch: 45,
bearing: -20,
bearing: -15,
antialias: true,
});
map.addControl(new maplibregl.NavigationControl(), "top-right");
// si on a une dernière vue en localStorage, on la remet ici
let hadLastView = false;
try {
const raw = localStorage.getItem(LAST_VIEW_KEY);
if (raw) {
const v = JSON.parse(raw);
if (
typeof v.lat === "number" &&
typeof v.lon === "number" &&
typeof v.zoom === "number"
) {
map.setCenter([v.lon, v.lat]);
map.setZoom(v.zoom);
hadLastView = true;
}
}
} catch (_) {
// ignore
}
setHasLastView(hadLastView);
map.addControl(new maplibregl.NavigationControl(), "top-right");
mapRef.current = map;
setMapReady(true);
// première vue viewParams
map.on("load", () => {
const vp = getViewFromMap(map);
setViewParams(vp);
});
// à chaque fin de move/zoom :
// - on maj viewParams (pour fetch)
// - on sauvegarde la vue dans localStorage
map.on("moveend", () => {
const vp = getViewFromMap(map);
setViewParams(vp);
try {
const center = vp.center;
localStorage.setItem(
LAST_VIEW_KEY,
JSON.stringify({
lat: center[1],
lon: center[0],
zoom: map.getZoom(),
})
);
} catch (_) {
// ignore
}
});
return () => {
markersRef.current.forEach((m) => m.marker.remove());
@ -75,75 +200,117 @@ export default function MapView() {
};
}, []);
/* ------------ CENTRER SUR POSITION USER -------------- */
useEffect(() => {
if (!mapReady || !mapRef.current) return;
const map = mapRef.current;
let cancelled = false;
function flyTo(lat, lon) {
if (cancelled) return;
map.flyTo({
center: [lon, lat],
zoom: 10,
speed: 1.4,
curve: 1.6,
});
}
async function fallbackIp() {
try {
const res = await fetch("/api/location"); // <-- adapte si ton endpoint est différent
if (!res.ok) return;
const data = await res.json(); // attendu: { lat: number, lon: number }
if (
typeof data.lat === "number" &&
typeof data.lon === "number" &&
!Number.isNaN(data.lat) &&
!Number.isNaN(data.lon)
) {
flyTo(data.lat, data.lon);
}
} catch (e) {
// pas grave, on garde le centre par défaut
console.warn("IP geo fallback failed:", e);
}
}
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition(
(pos) => {
if (cancelled) return;
flyTo(pos.coords.latitude, pos.coords.longitude);
},
() => {
if (cancelled) return;
fallbackIp();
},
{ enableHighAccuracy: true, timeout: 8000 }
);
} else {
fallbackIp();
}
return () => {
cancelled = true;
};
}, [mapReady]);
/* ------------ LOAD POSTS -------------- */
/* ------------ POSITION UTILISATEUR (GPS/IP) -------------- */
useEffect(() => {
const map = mapRef.current;
if (!map) return;
let cancelled = false;
function saveUserPos(lat, lon) {
if (cancelled) return;
const coords = [lon, lat];
setUserPosition(coords);
try {
localStorage.setItem(LAST_POS_KEY, JSON.stringify({ lat, lon }));
} catch (_) {}
// si on N'A PAS de lastView, on utilise le geo pour centrer
if (!hasLastView) {
map.flyTo({
center: coords,
zoom: 9,
speed: 1.2,
curve: 1.5,
essential: true,
});
}
}
// 1) dernière position utilisateur (pas la vue)
try {
const raw = localStorage.getItem(LAST_POS_KEY);
if (raw) {
const p = JSON.parse(raw);
if (typeof p.lat === "number" && typeof p.lon === "number") {
setUserPosition([p.lon, p.lat]);
}
}
} catch (_) {}
// 2) GPS 3) IP backend
function viaIpFallback() {
(async () => {
const ipCoords = await fetchIpLocation();
if (!ipCoords || cancelled) return;
const [lon, lat] = ipCoords;
saveUserPos(lat, lon);
})();
}
if ("geolocation" in navigator) {
navigator.geolocation.getCurrentPosition(
(pos) => {
if (cancelled) return;
saveUserPos(pos.coords.latitude, pos.coords.longitude);
},
() => {
if (cancelled) return;
viaIpFallback();
},
{
enableHighAccuracy: true,
timeout: 8000,
maximumAge: 30000,
}
);
} else {
viaIpFallback();
}
return () => {
cancelled = true;
};
}, [hasLastView]);
/* ------------ SOUS-CATEGORIES (bas) -------------- */
const bottomCategories =
CATEGORY_MAP[mainFilter] || CATEGORY_MAP.Default;
useEffect(() => {
if (!bottomCategories.length) return;
if (!subFilter || !bottomCategories.includes(subFilter)) {
setSubFilter(bottomCategories[0]);
}
}, [mainFilter, bottomCategories, subFilter]);
/* ------------ LOAD POSTS POUR LA ZONE COURANTE + FILTRES -------------- */
useEffect(() => {
const map = mapRef.current;
if (!map) return;
if (!viewParams.center) return;
let cancelled = false;
async function load() {
try {
setStatus("Loading posts...");
const raw = await fetchPosts();
const posts = Array.isArray(raw) ? raw : raw?.items || [];
const [lng, lat] = viewParams.center;
const radiusKm = viewParams.radiusKm || 250;
const catCode = categoryCode(mainFilter);
const subCatParam =
subFilter && subFilter.toUpperCase() !== "ALL" ? subFilter : "";
const posts = await fetchPosts({
category: catCode,
subCategory: subCatParam,
time: timeFilter,
lat,
lon: lng,
radiusKm,
});
if (cancelled) return;
@ -151,13 +318,20 @@ export default function MapView() {
markersRef.current = [];
expandedElRef.current = null;
const bounds = new maplibregl.LngLatBounds();
let hasCoords = false;
posts.forEach((post) => {
const coords = getCoords(post);
if (!coords) return;
const [lngP, latP] = coords;
if (
typeof lngP !== "number" ||
typeof latP !== "number" ||
Number.isNaN(lngP) ||
Number.isNaN(latP)
) {
return;
}
const { title, body, short } = extract(post);
const compactHTML = `
@ -173,7 +347,9 @@ export default function MapView() {
<div class="marker-dot"></div>
<span class="marker-expanded-title">${title}</span>
</div>
<div class="marker-expanded-body">${body}</div>
<div class="marker-expanded-body">
${body}
</div>
</div>
`;
@ -181,7 +357,10 @@ export default function MapView() {
el.className = "post-marker post-marker-compact";
el.innerHTML = compactHTML;
const marker = new maplibregl.Marker({ element: el, anchor: "bottom" })
const marker = new maplibregl.Marker({
element: el,
anchor: "bottom",
})
.setLngLat(coords)
.addTo(map);
@ -215,15 +394,8 @@ export default function MapView() {
e.preventDefault();
toggle();
});
bounds.extend(coords);
hasCoords = true;
});
if (hasCoords) {
map.fitBounds(bounds, { padding: 40, maxZoom: 13 });
}
setStatus(posts.length ? "" : "No posts found.");
} catch (err) {
console.error("Erreur chargement posts:", err);
@ -236,9 +408,134 @@ export default function MapView() {
return () => {
cancelled = true;
};
}, [viewParams, mainFilter, timeFilter, subFilter, reloadToken]);
/* ------------ WEBSOCKET : RELOAD SUR new_post -------------- */
useEffect(() => {
const proto = window.location.protocol === "https:" ? "wss" : "ws";
const host =
window.location.port === "5173"
? `${window.location.hostname}:8081`
: window.location.host;
const wsUrl = `${proto}://${host}/ws`;
let ws;
try {
ws = new WebSocket(wsUrl);
} catch (e) {
console.warn("WebSocket init error:", e);
return;
}
ws.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data);
if (msg.type === "new_post") {
setReloadToken((n) => n + 1);
}
} catch (_) {}
};
ws.onerror = (e) => {
console.warn("WebSocket error:", e);
};
return () => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close();
}
};
}, []);
const bottomCategories = CATEGORY_MAP[mainFilter] || CATEGORY_MAP.Default;
/* ------------ ACTIONS UI -------------- */
const handleFlyToMe = () => {
const map = mapRef.current;
if (!map || !userPosition) return;
map.flyTo({
center: userPosition,
zoom: 9,
speed: 1.1,
curve: 1.5,
essential: true,
});
};
const handlePickCenter = () => {
const map = mapRef.current;
if (!map) return;
const center = map.getCenter();
setDraftCoords([center.lng, center.lat]);
};
const handleOpenCreate = () => {
setIsCreating(true);
setSaveError("");
setDraftTitle("");
setDraftBody("");
const main = mainFilter === "Default" ? "News" : mainFilter;
setDraftCategory(main);
const list = CATEGORY_MAP[main] || CATEGORY_MAP.Default;
setDraftSubCategory(list[0]);
setDraftCoords(null);
};
const handleSubmitPost = async () => {
if (isSaving) return;
setSaveError("");
const map = mapRef.current;
if (!map) return;
let coords = draftCoords;
if (!coords) {
const center = map.getCenter();
coords = [center.lng, center.lat];
}
if (!coords) {
setSaveError("Position inconnue. Déplace la carte et tap sur la cible.");
return;
}
const [lng, lat] = coords;
if (!draftTitle.trim()) {
setSaveError("Le titre est requis.");
return;
}
const categoryUpper =
draftCategory && draftCategory.toUpperCase() !== "DEFAULT"
? draftCategory.toUpperCase()
: "NEWS";
const payload = {
title: draftTitle.trim(),
snippet: draftBody.trim() || draftTitle.trim(),
category: categoryUpper,
sub_category: draftSubCategory || "ALL",
lat,
lon: lng,
author: "tommy",
};
try {
setIsSaving(true);
await createPost(payload);
setIsSaving(false);
setIsCreating(false);
setReloadToken((n) => n + 1);
} catch (e) {
console.error("createPost error:", e);
setIsSaving(false);
setSaveError("Erreur lors de la création du post.");
}
};
/* ------------ RENDER -------------- */
return (
<div className="map-view">
@ -254,14 +551,29 @@ export default function MapView() {
<button className="chip-hex">Zoom Out</button>
<button className="chip-hex">Swall</button>
</div>
<button className="chip-pill top-newpost" onClick={handleOpenCreate}>
New post
</button>
</div>
{/* LEFT */}
{/* LEFT — temps */}
<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>
{[
["NOW", "Now"],
["TODAY", "Today"],
["RECENT", "Recent"],
["PAST", "Past"],
].map(([code, label]) => (
<button
key={code}
className={
timeFilter === code ? "chip-round chip-selected" : "chip-round"
}
onClick={() => setTimeFilter(code)}
>
{label}
</button>
))}
</div>
{/* RIGHT — filtres principaux */}
@ -279,19 +591,119 @@ export default function MapView() {
))}
</div>
{/* BOTTOM — arc/orbite sous le globe */}
{/* BOTTOM — arc courbé inversé autour du globe */}
<div className="map-overlay map-overlay-bottom">
{bottomCategories.map((c, idx) => (
{bottomCategories.map((c) => (
<button
key={c}
className={
idx === 0 ? "chip-egg chip-egg-active" : "chip-egg"
subFilter === c ? "chip-egg chip-egg-active" : "chip-egg"
}
onClick={() => setSubFilter(c)}
>
{c}
</button>
))}
</div>
{/* BOUTON "MY SPOT" */}
<div className="map-overlay map-overlay-myloc">
<button
className="chip-pill"
disabled={!userPosition}
onClick={handleFlyToMe}
>
My spot
</button>
</div>
{/* CROSSHAIR SUR LA MAP (au-dessus du popup) */}
{isCreating && (
<div className="map-overlay map-crosshair" onClick={handlePickCenter}>
<div className="crosshair-aim" />
</div>
)}
{/* PANEL CREATION DE POST (1/5 du bas) */}
{isCreating && (
<div className="map-overlay create-post-panel">
<div className="create-post-header">
<span>Create wire</span>
<button
className="create-close"
onClick={() => setIsCreating(false)}
>
</button>
</div>
<div className="create-row">
<span className="crosshair-label">
Déplace la carte sous la cible, puis tap sur la cible pour fixer
la position.
{draftCoords ? " ✓ Position choisie" : ""}
</span>
</div>
<div className="create-row">
<select
value={draftCategory}
onChange={(e) => {
const cat = e.target.value;
setDraftCategory(cat);
const list = CATEGORY_MAP[cat] || CATEGORY_MAP.Default;
setDraftSubCategory(list[0]);
}}
>
{["News", "Friends", "Events", "Market"].map((cat) => (
<option key={cat} value={cat}>
{cat}
</option>
))}
</select>
<select
value={draftSubCategory}
onChange={(e) => setDraftSubCategory(e.target.value)}
>
{(CATEGORY_MAP[draftCategory] || CATEGORY_MAP.Default).map(
(sub) => (
<option key={sub} value={sub}>
{sub}
</option>
)
)}
</select>
</div>
<input
className="create-input"
placeholder="Short title..."
value={draftTitle}
onChange={(e) => setDraftTitle(e.target.value)}
/>
<textarea
className="create-textarea"
rows={2}
placeholder="Add a bit more context (optional)..."
value={draftBody}
onChange={(e) => setDraftBody(e.target.value)}
/>
{saveError && <div className="create-error">{saveError}</div>}
<div className="create-actions">
<button
className="chip-pill"
onClick={handleSubmitPost}
disabled={isSaving}
>
{isSaving ? "Posting..." : "Post as tommy"}
</button>
</div>
</div>
)}
</div>
);
}

View File

@ -1,5 +1,5 @@
/* ==========================================
SOCIOWIRE OVERLAY + ARC AUTOUR DU GLOBE
SOCIOWIRE OVERLAY + ARC + CREATE PANEL
========================================== */
.map-overlay {
@ -43,6 +43,11 @@
gap: 0.35rem;
}
/* bouton New post dans la top bar */
.top-newpost {
margin-left: 0.4rem;
}
/* --------- LEFT --------- */
.map-overlay-left {
@ -65,41 +70,178 @@
gap: 0.5rem;
}
/* Bouton principal sélectionné (News/Friends/Events/Market) */
/* Bouton principal sélectionné */
.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) --------- */
/* --------- BOTTOM — ARC (INVERSE AUTOUR DU GLOBE) --------- */
.map-overlay-bottom {
bottom: 3.2rem; /* au-dessus de la barre Android */
bottom: 3.2rem;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 0.4rem;
}
/* On crée un arc en jouant avec translateY + rotate */
/* courbure inversée : centre plus bas, côtés plus hauts */
.map-overlay-bottom .chip-egg:nth-child(1) {
transform: translateY(8px) rotate(-12deg);
transform: translateY(-8px) rotate(-12deg);
}
.map-overlay-bottom .chip-egg:nth-child(2) {
transform: translateY(4px) rotate(-6deg);
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);
transform: translateY(-4px) rotate(6deg);
}
.map-overlay-bottom .chip-egg:nth-child(5) {
transform: translateY(8px) rotate(12deg);
transform: translateY(-8px) rotate(12deg);
}
/* --------- CHIPS (boutons) --------- */
/* --------- BOUTON "MY SPOT" --------- */
.map-overlay-myloc {
right: 0.7rem;
bottom: 6rem;
}
/* --------- CROSSHAIR SUR LA MAP --------- */
.map-crosshair {
left: 50%;
top: 40%; /* au-dessus du popup */
transform: translate(-50%, -50%);
}
.crosshair-aim {
width: 34px;
height: 34px;
border-radius: 999px;
border: 1px solid #38bdf8;
position: relative;
box-shadow: 0 0 14px rgba(56, 189, 248, 0.9);
}
.crosshair-aim::before,
.crosshair-aim::after {
content: "";
position: absolute;
left: 50%;
top: 50%;
background: #38bdf8;
transform: translate(-50%, -50%);
}
.crosshair-aim::before {
width: 70%;
height: 1px;
}
.crosshair-aim::after {
width: 1px;
height: 70%;
}
/* --------- CREATE POST PANEL (1/5 du bas) --------- */
.create-post-panel {
left: 50%;
bottom: 20%;
transform: translateX(-50%);
width: 92%;
max-width: 520px;
background: rgba(15, 23, 42, 0.96);
border-radius: 16px;
border: 1px solid rgba(148, 163, 184, 0.9);
box-shadow:
0 16px 40px rgba(0, 0, 0, 0.9),
0 0 24px rgba(56, 189, 248, 0.6);
padding: 0.6rem 0.7rem 0.7rem 0.7rem;
}
.create-post-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.8rem;
margin-bottom: 0.4rem;
color: #e5e7eb;
}
.create-close {
background: transparent;
border: none;
color: #9ca3af;
font-size: 0.85rem;
padding: 0.1rem 0.3rem;
cursor: pointer;
}
.create-row {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.4rem;
}
.crosshair-label {
font-size: 0.75rem;
color: #cbd5f5;
}
.create-row select {
flex: 1;
background: #020617;
border-radius: 999px;
border: 1px solid #4b5563;
padding: 0.35rem 0.7rem;
font-size: 0.75rem;
color: #e5e7eb;
}
.create-input {
width: 100%;
margin-bottom: 0.3rem;
border-radius: 999px;
border: 1px solid #4b5563;
padding: 0.35rem 0.7rem;
background: #020617;
color: #e5e7eb;
font-size: 0.8rem;
}
.create-textarea {
width: 100%;
border-radius: 0.75rem;
border: 1px solid #4b5563;
padding: 0.35rem 0.7rem;
background: #020617;
color: #e5e7eb;
resize: none;
font-size: 0.78rem;
}
.create-error {
margin-top: 0.25rem;
font-size: 0.7rem;
color: #f97373;
}
.create-actions {
margin-top: 0.4rem;
display: flex;
justify-content: flex-end;
}
/* --------- STYLES DES CHIPS --------- */
.chip-round,
.chip-pill,
@ -117,19 +259,16 @@
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;
}