sw-fe/src/components/Map/MapView.jsx

321 lines
9.1 KiB
JavaScript

import React, { useEffect, useState } from "react";
import "maplibre-gl/dist/maplibre-gl.css";
import "../../styles/mapMarkers.css";
import "../../styles/overlays.css";
import "../../styles/posts.css";
import { createPost } from "../../api/client";
import {
FILTER_MAIN_CATEGORIES,
FILTER_CATEGORY_MAP,
CREATE_CATEGORY_MAP,
} from "./mapConfig";
import { useMapCore } from "./useMapCore";
import { useUserPosition } from "./useUserPosition";
import { usePostsEngine } from "./usePostsEngine";
import PostList from "../Posts/PostList";
export default function MapView() {
const {
containerRef,
mapRef,
markersRef,
expandedElRef,
viewParams,
hasLastView,
} = useMapCore();
const [mainFilter, setMainFilter] = useState("All");
const [timeFilter, setTimeFilter] = useState("RECENT");
const [subFilter, setSubFilter] = useState("All");
const userPosition = useUserPosition(mapRef, hasLastView);
const {
status,
visiblePosts,
loadingPosts,
loadError,
handleIncomingPost,
} = usePostsEngine({
mapRef,
viewParams,
mainFilter,
subFilter,
timeFilter,
markersRef,
expandedElRef,
});
const [selectedPost, setSelectedPost] = useState(null);
const [isCreating, setIsCreating] = useState(false);
const [draftTitle, setDraftTitle] = useState("");
const [draftBody, setDraftBody] = useState("");
const [draftCategory, setDraftCategory] = useState("News");
const [draftSubCategory, setDraftSubCategory] = useState(
CREATE_CATEGORY_MAP.News[0]
);
const [draftCoords, setDraftCoords] = useState(null);
const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState("");
const bottomCategories = FILTER_CATEGORY_MAP[mainFilter] || ["All"];
useEffect(() => {
if (!bottomCategories.length) return;
if (!subFilter || !bottomCategories.includes(subFilter)) {
setSubFilter("All");
}
}, [mainFilter, bottomCategories, subFilter]);
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 {
return;
}
ws.onmessage = (ev) => {
try {
const msg = JSON.parse(ev.data);
if (msg.type === "new_post" && msg.post) {
handleIncomingPost(msg.post);
}
} catch {}
};
return () => ws && ws.close();
}, [handleIncomingPost]);
const handleFlyToMe = () => {
const map = mapRef.current;
if (!map || !userPosition) return;
map.flyTo({ center: userPosition, zoom: 9, speed: 1.2 });
};
const handlePickCenter = () => {
const map = mapRef.current;
if (!map) return;
const center = map.getCenter();
setDraftCoords([center.lng, center.lat]);
};
const handleOpenCreate = () => {
setIsCreating(true);
setDraftTitle("");
setDraftBody("");
const main = mainFilter === "All" ? "News" : mainFilter;
setDraftCategory(main);
const list = CREATE_CATEGORY_MAP[main] || CREATE_CATEGORY_MAP.News;
setDraftSubCategory(list[0]);
setDraftCoords(null);
};
const handleSearchClick = () => {
alert("🔍 Search feature coming soon!");
};
const handleSubmitPost = async () => {
if (isSaving) return;
setSaveError("");
const map = mapRef.current;
if (!map) return;
let coords = draftCoords || [map.getCenter().lng, map.getCenter().lat];
const [lng, lat] = coords;
if (!draftTitle.trim()) {
setSaveError("Title required.");
return;
}
try {
setIsSaving(true);
await createPost({
title: draftTitle.trim(),
snippet: draftBody.trim() || draftTitle.trim(),
category: draftCategory.toUpperCase(),
sub_category: draftSubCategory || "ALL",
lat,
lon: lng,
author: "tommy",
});
setIsSaving(false);
setIsCreating(false);
} catch {
setIsSaving(false);
setSaveError("Error creating post.");
}
};
const handleSelectPost = (post) => {
setSelectedPost(post);
const map = mapRef.current;
if (!map) return;
const lng = post.lon ?? post.lng;
const lat = post.lat ?? post.latitude;
if (lng && lat) map.flyTo({ center: [lng, lat], zoom: 8, speed: 1.4 });
};
return (
<div className="map-view">
<div ref={containerRef} className="map-container" />
{status && <div className="map-status">{status}</div>}
{/* TOP BAR SIMPLIFIÉE */}
<div className="map-overlay map-overlay-top">
<button className="chip-pill" onClick={handleSearchClick}>
Look at
</button>
<button className="chip-pill" onClick={handleOpenCreate}>
Place your wire
</button>
</div>
{/* LEFT */}
<div className="map-overlay map-overlay-left">
{[
["NOW", "Now"],
["TODAY", "Today"],
["RECENT", "Recently"],
["PAST", "Past"],
].map(([code, label]) => (
<button
key={code}
className={
timeFilter === code ? "chip-round chip-selected" : "chip-round"
}
onClick={() => setTimeFilter(code)}
>
{label}
</button>
))}
</div>
{/* RIGHT */}
<div className="map-overlay map-overlay-right">
{FILTER_MAIN_CATEGORIES.map((cat) => (
<button
key={cat}
className={
mainFilter === cat ? "chip-pill chip-selected" : "chip-pill"
}
onClick={() => setMainFilter(cat)}
>
{cat}
</button>
))}
</div>
{/* BOTTOM SUBCATEGORIES */}
<div className="map-overlay map-overlay-bottom">
{bottomCategories.map((c) => (
<button
key={c}
className={
subFilter === c ? "chip-egg chip-egg-active" : "chip-egg"
}
onClick={() => setSubFilter(c)}
>
{c}
</button>
))}
</div>
{/* MY SPOT */}
<div className="map-overlay map-overlay-myloc">
<button
className="chip-pill"
disabled={!userPosition}
onClick={handleFlyToMe}
>
My spot
</button>
</div>
{/* CREATE POST */}
{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>
<span className="crosshair-label">
Move the map, then tap to fix the position.
</span>
<div className="create-row">
<select
value={draftCategory}
onChange={(e) => {
const cat = e.target.value;
setDraftCategory(cat);
setDraftSubCategory(
(CREATE_CATEGORY_MAP[cat] || CREATE_CATEGORY_MAP.News)[0]
);
}}
>
{["News", "Friends", "Events", "Market"].map((cat) => (
<option key={cat}>{cat}</option>
))}
</select>
<select
value={draftSubCategory}
onChange={(e) => setDraftSubCategory(e.target.value)}
>
{(CREATE_CATEGORY_MAP[draftCategory] ||
CREATE_CATEGORY_MAP.News).map((sub) => (
<option key={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="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>
)}
{/* SOCIOWALL FIXE + CHAT */}
<div className="map-overlay map-overlay-wall">
<div className="sociowall-panel">
<div className="sociowall-header">Sociowall</div>
<PostList
posts={visiblePosts}
loading={loadingPosts}
error={loadError}
selectedPost={selectedPost}
onSelectPost={handleSelectPost}
/>
</div>
<div className="chat-panel">
<div className="chat-header">Chat</div>
<ul className="chat-users">
<li>Yan</li>
<li>Marc</li>
<li>Fiso</li>
</ul>
</div>
</div>
</div>
);
}