update frontend

This commit is contained in:
Your Name 2025-12-09 20:42:51 -05:00
parent 721e17d3a6
commit 2bbb74de35
4 changed files with 59 additions and 186 deletions

View File

@ -1,7 +1,8 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import "maplibre-gl/dist/maplibre-gl.css"; import "maplibre-gl/dist/maplibre-gl.css";
import "../../styles/mapMarkers.css"; import "../../styles/mapMarkers.css";
import "../../styles/mapLayout.css"; import "../../styles/overlays.css";
import "../../styles/posts.css";
import { createPost } from "../../api/client"; import { createPost } from "../../api/client";
import { import {
@ -15,7 +16,6 @@ import { usePostsEngine } from "./usePostsEngine";
import PostList from "../Posts/PostList"; import PostList from "../Posts/PostList";
export default function MapView() { export default function MapView() {
// MAP + refs
const { const {
containerRef, containerRef,
mapRef, mapRef,
@ -25,15 +25,12 @@ export default function MapView() {
hasLastView, hasLastView,
} = useMapCore(); } = useMapCore();
// FILTRES
const [mainFilter, setMainFilter] = useState("All"); const [mainFilter, setMainFilter] = useState("All");
const [timeFilter, setTimeFilter] = useState("RECENT"); const [timeFilter, setTimeFilter] = useState("RECENT");
const [subFilter, setSubFilter] = useState("All"); const [subFilter, setSubFilter] = useState("All");
// USER POSITION
const userPosition = useUserPosition(mapRef, hasLastView); const userPosition = useUserPosition(mapRef, hasLastView);
// POSTS ENGINE (markers + Sociowall)
const { const {
status, status,
visiblePosts, visiblePosts,
@ -50,10 +47,8 @@ export default function MapView() {
expandedElRef, expandedElRef,
}); });
// SOCIOWALL selection
const [selectedPost, setSelectedPost] = useState(null); const [selectedPost, setSelectedPost] = useState(null);
// NEW POST (overlay)
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [draftTitle, setDraftTitle] = useState(""); const [draftTitle, setDraftTitle] = useState("");
const [draftBody, setDraftBody] = useState(""); const [draftBody, setDraftBody] = useState("");
@ -65,8 +60,6 @@ export default function MapView() {
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState(""); const [saveError, setSaveError] = useState("");
/* ================= FILTRES (bas) ================= */
const bottomCategories = FILTER_CATEGORY_MAP[mainFilter] || ["All"]; const bottomCategories = FILTER_CATEGORY_MAP[mainFilter] || ["All"];
useEffect(() => { useEffect(() => {
@ -76,59 +69,34 @@ export default function MapView() {
} }
}, [mainFilter, bottomCategories, subFilter]); }, [mainFilter, bottomCategories, subFilter]);
/* ================= WEBSOCKET ================= */
useEffect(() => { useEffect(() => {
const proto = window.location.protocol === "https:" ? "wss" : "ws"; const proto = window.location.protocol === "https:" ? "wss" : "ws";
const host = const host =
window.location.port === "5173" window.location.port === "5173"
? `${window.location.hostname}:8081` ? `${window.location.hostname}:8081`
: window.location.host; : window.location.host;
const wsUrl = `${proto}://${host}/ws`; const wsUrl = `${proto}://${host}/ws`;
let ws; let ws;
try { try {
ws = new WebSocket(wsUrl); ws = new WebSocket(wsUrl);
} catch (e) { } catch {
console.warn("WebSocket init error:", e);
return; return;
} }
ws.onmessage = (ev) => { ws.onmessage = (ev) => {
try { try {
const msg = JSON.parse(ev.data); const msg = JSON.parse(ev.data);
if (msg.type === "new_post" && msg.post) { if (msg.type === "new_post" && msg.post) {
handleIncomingPost(msg.post); handleIncomingPost(msg.post);
} }
} catch { } catch {}
// ignore
}
};
ws.onerror = (e) => {
console.warn("WebSocket error:", e);
};
return () => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close();
}
}; };
return () => ws && ws.close();
}, [handleIncomingPost]); }, [handleIncomingPost]);
/* ================= ACTIONS UI ================= */
const handleFlyToMe = () => { const handleFlyToMe = () => {
const map = mapRef.current; const map = mapRef.current;
if (!map || !userPosition) return; if (!map || !userPosition) return;
map.flyTo({ map.flyTo({ center: userPosition, zoom: 9, speed: 1.2 });
center: userPosition,
zoom: 9,
speed: 1.2,
curve: 1.5,
essential: true,
});
}; };
const handlePickCenter = () => { const handlePickCenter = () => {
@ -140,10 +108,8 @@ export default function MapView() {
const handleOpenCreate = () => { const handleOpenCreate = () => {
setIsCreating(true); setIsCreating(true);
setSaveError("");
setDraftTitle(""); setDraftTitle("");
setDraftBody(""); setDraftBody("");
const main = mainFilter === "All" ? "News" : mainFilter; const main = mainFilter === "All" ? "News" : mainFilter;
setDraftCategory(main); setDraftCategory(main);
const list = CREATE_CATEGORY_MAP[main] || CREATE_CATEGORY_MAP.News; const list = CREATE_CATEGORY_MAP[main] || CREATE_CATEGORY_MAP.News;
@ -151,55 +117,37 @@ export default function MapView() {
setDraftCoords(null); setDraftCoords(null);
}; };
const handleSearchClick = () => {
alert("🔍 Search feature coming soon!");
};
const handleSubmitPost = async () => { const handleSubmitPost = async () => {
if (isSaving) return; if (isSaving) return;
setSaveError(""); setSaveError("");
const map = mapRef.current; const map = mapRef.current;
if (!map) return; if (!map) return;
let coords = draftCoords || [map.getCenter().lng, map.getCenter().lat];
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; const [lng, lat] = coords;
if (!draftTitle.trim()) { if (!draftTitle.trim()) {
setSaveError("Le titre est requis."); setSaveError("Title required.");
return; 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 { try {
setIsSaving(true); setIsSaving(true);
await createPost(payload); await createPost({
title: draftTitle.trim(),
snippet: draftBody.trim() || draftTitle.trim(),
category: draftCategory.toUpperCase(),
sub_category: draftSubCategory || "ALL",
lat,
lon: lng,
author: "tommy",
});
setIsSaving(false); setIsSaving(false);
setIsCreating(false); setIsCreating(false);
} catch (e) { } catch {
console.error("createPost error:", e);
setIsSaving(false); setIsSaving(false);
setSaveError("Erreur lors de la création du post."); setSaveError("Error creating post.");
} }
}; };
@ -207,59 +155,27 @@ export default function MapView() {
setSelectedPost(post); setSelectedPost(post);
const map = mapRef.current; const map = mapRef.current;
if (!map) return; if (!map) return;
const lng = post.lon ?? post.lng;
const lng = const lat = post.lat ?? post.latitude;
post.lon ?? if (lng && lat) map.flyTo({ center: [lng, lat], zoom: 8, speed: 1.4 });
post.lng ??
(Array.isArray(post.coords) ? post.coords[0] : null);
const lat =
post.lat ??
post.latitude ??
(Array.isArray(post.coords) ? post.coords[1] : null);
if (typeof lng === "number" && typeof lat === "number") {
map.flyTo({
center: [lng, lat],
zoom: Math.max(map.getZoom(), 8),
speed: 1.4,
curve: 1.6,
essential: true,
});
}
// ouvre le gros post si on a le marker
const markerEntry = markersRef.current.find((m) => m.id === post.id);
if (markerEntry && markerEntry.el) {
const root = markerEntry.el;
const isExpanded = root.classList.contains("post-pin--expanded");
if (!isExpanded) {
root.click();
}
}
}; };
/* ================= RENDER ================= */
return ( return (
<div className="map-view"> <div className="map-view">
<div ref={containerRef} className="map-container" /> <div ref={containerRef} className="map-container" />
{status && <div className="map-status">{status}</div>} {status && <div className="map-status">{status}</div>}
{/* TOP */} {/* TOP BAR SIMPLIFIÉE */}
<div className="map-overlay map-overlay-top"> <div className="map-overlay map-overlay-top">
<div className="lookat-box">Look at</div> <button className="chip-pill" onClick={handleSearchClick}>
<div className="wire-title">Place your wire</div> Look at
<div className="zoom-buttons"> </button>
<button className="chip-hex">Zoom In</button> <button className="chip-pill" onClick={handleOpenCreate}>
<button className="chip-hex">Zoom Out</button> Place your wire
<button className="chip-hex">Swall</button>
</div>
<button className="chip-pill top-newpost" onClick={handleOpenCreate}>
New post
</button> </button>
</div> </div>
{/* LEFT — time filters */} {/* LEFT */}
<div className="map-overlay map-overlay-left"> <div className="map-overlay map-overlay-left">
{[ {[
["NOW", "Now"], ["NOW", "Now"],
@ -279,7 +195,7 @@ export default function MapView() {
))} ))}
</div> </div>
{/* RIGHT — main categories */} {/* RIGHT */}
<div className="map-overlay map-overlay-right"> <div className="map-overlay map-overlay-right">
{FILTER_MAIN_CATEGORIES.map((cat) => ( {FILTER_MAIN_CATEGORIES.map((cat) => (
<button <button
@ -294,7 +210,7 @@ export default function MapView() {
))} ))}
</div> </div>
{/* BOTTOM — subcategories */} {/* BOTTOM SUBCATEGORIES */}
<div className="map-overlay map-overlay-bottom"> <div className="map-overlay map-overlay-bottom">
{bottomCategories.map((c) => ( {bottomCategories.map((c) => (
<button <button
@ -320,96 +236,66 @@ export default function MapView() {
</button> </button>
</div> </div>
{/* CROSSHAIR */} {/* CREATE POST */}
{isCreating && (
<div className="map-overlay map-crosshair" onClick={handlePickCenter}>
<div className="crosshair-aim" />
</div>
)}
{/* CREATE POST PANEL */}
{isCreating && ( {isCreating && (
<div className="map-overlay create-post-panel"> <div className="map-overlay create-post-panel">
<div className="create-post-header"> <div className="create-post-header">
<span>Create wire</span> <span>Create wire</span>
<button <button className="create-close" onClick={() => setIsCreating(false)}>
className="create-close"
onClick={() => setIsCreating(false)}
>
</button> </button>
</div> </div>
<span className="crosshair-label">
<div className="create-row"> Move the map, then tap to fix the position.
<span className="crosshair-label"> </span>
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"> <div className="create-row">
<select <select
value={draftCategory} value={draftCategory}
onChange={(e) => { onChange={(e) => {
const cat = e.target.value; const cat = e.target.value;
setDraftCategory(cat); setDraftCategory(cat);
const list = setDraftSubCategory(
CREATE_CATEGORY_MAP[cat] || CREATE_CATEGORY_MAP.News; (CREATE_CATEGORY_MAP[cat] || CREATE_CATEGORY_MAP.News)[0]
setDraftSubCategory(list[0]); );
}} }}
> >
{["News", "Friends", "Events", "Market"].map((cat) => ( {["News", "Friends", "Events", "Market"].map((cat) => (
<option key={cat} value={cat}> <option key={cat}>{cat}</option>
{cat}
</option>
))} ))}
</select> </select>
<select <select
value={draftSubCategory} value={draftSubCategory}
onChange={(e) => setDraftSubCategory(e.target.value)} onChange={(e) => setDraftSubCategory(e.target.value)}
> >
{(CREATE_CATEGORY_MAP[draftCategory] || {(CREATE_CATEGORY_MAP[draftCategory] ||
CREATE_CATEGORY_MAP.News CREATE_CATEGORY_MAP.News).map((sub) => (
).map((sub) => ( <option key={sub}>{sub}</option>
<option key={sub} value={sub}>
{sub}
</option>
))} ))}
</select> </select>
</div> </div>
<input <input
className="create-input" className="create-input"
placeholder="Short title..." placeholder="Short title..."
value={draftTitle} value={draftTitle}
onChange={(e) => setDraftTitle(e.target.value)} onChange={(e) => setDraftTitle(e.target.value)}
/> />
<textarea <textarea
className="create-textarea" className="create-textarea"
rows={2} rows={2}
placeholder="Add a bit more context (optional)..." placeholder="Context (optional)..."
value={draftBody} value={draftBody}
onChange={(e) => setDraftBody(e.target.value)} onChange={(e) => setDraftBody(e.target.value)}
/> />
{saveError && <div className="create-error">{saveError}</div>} {saveError && <div className="create-error">{saveError}</div>}
<div className="create-actions"> <div className="create-actions">
<button <button className="chip-pill" onClick={handleSubmitPost} disabled={isSaving}>
className="chip-pill"
onClick={handleSubmitPost}
disabled={isSaving}
>
{isSaving ? "Posting..." : "Post as tommy"} {isSaving ? "Posting..." : "Post as tommy"}
</button> </button>
</div> </div>
</div> </div>
)} )}
{/* SOCIOWALL + CHAT EN BAS */} {/* SOCIOWALL FIXE + CHAT */}
<div className="map-overlay map-overlay-wall"> <div className="map-overlay map-overlay-wall">
<div className="sociowall-panel"> <div className="sociowall-panel">
<div className="sociowall-header">Sociowall</div> <div className="sociowall-header">Sociowall</div>
@ -432,4 +318,4 @@ export default function MapView() {
</div> </div>
</div> </div>
); );
} }

View File

@ -9,7 +9,7 @@ import "./styles/map.css";
import "./styles/markers.css"; import "./styles/markers.css";
import "./styles/popup.css"; import "./styles/popup.css";
import "./styles/overlays.css"; import "./styles/overlays.css";
import "./styles/posts.css"; import "./styles/posts.css"; // Sociowall + chat
ReactDOM.createRoot(document.getElementById("root")).render( ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode> <React.StrictMode>

View File

@ -1,5 +1,6 @@
/* ========================================== /* ==========================================
SOCIOWIRE OVERLAY + ARC + CREATE PANEL SOCIOWIRE OVERLAYS (TOP / LEFT / RIGHT /
BOTTOM / MY SPOT / CREATE PANEL)
========================================== */ ========================================== */
.map-overlay { .map-overlay {
@ -24,12 +25,9 @@
} }
.lookat-box { .lookat-box {
padding: 0.35rem 0.9rem; /* maintenant on utilise un bouton chip, cette classe sert juste au texte au centre si besoin */
background: rgba(0, 10, 30, 0.85);
border-radius: 999px;
font-size: 0.75rem; font-size: 0.75rem;
border: 1px solid rgba(56, 189, 248, 0.5); color: #93c5fd;
color: #dbeafe;
} }
.wire-title { .wire-title {
@ -38,16 +36,6 @@
color: #93c5fd; color: #93c5fd;
} }
.zoom-buttons {
display: flex;
gap: 0.35rem;
}
/* bouton New post dans la top bar */
.top-newpost {
margin-left: 0.4rem;
}
/* --------- LEFT --------- */ /* --------- LEFT --------- */
.map-overlay-left { .map-overlay-left {
@ -80,8 +68,7 @@
/* --------- BOTTOM — ARC (INVERSE AUTOUR DU GLOBE) --------- */ /* --------- BOTTOM — ARC (INVERSE AUTOUR DU GLOBE) --------- */
.map-overlay-bottom { .map-overlay-bottom {
/* on remonte les œufs pour les placer au-dessus du Sociowall */ bottom: 3.2rem;
bottom: 5.8rem; /* avant ~3.2rem */
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
display: flex; display: flex;

View File

@ -11,7 +11,7 @@
gap: 0.6rem; gap: 0.6rem;
align-items: stretch; align-items: stretch;
pointer-events: none; pointer-events: none;
z-index: 10; /* plus bas que les autres overlays */ z-index: 10; /* un peu plus bas que les autres overlays */
} }
.map-overlay-wall * { .map-overlay-wall * {
@ -23,7 +23,7 @@
.sociowall-panel { .sociowall-panel {
flex: 1.5; flex: 1.5;
min-height: 90px; min-height: 90px;
max-height: 150px; /* hauteur bloquée → nenvahit pas la map */ max-height: 150px; /* HAUTEUR FIXE → nenvahit pas la map */
background: rgba(15, 23, 42, 0.96); background: rgba(15, 23, 42, 0.96);
border-radius: 14px; border-radius: 14px;
border: 1px solid rgba(148, 163, 184, 0.9); border: 1px solid rgba(148, 163, 184, 0.9);
@ -48,7 +48,7 @@
.post-list { .post-list {
flex: 1; flex: 1;
overflow-y: auto; /* cest ici que ça scrolle */ overflow-y: auto; /* cest ICI que ça scrolle */
padding-right: 0.15rem; padding-right: 0.15rem;
} }