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"; import { useAuth } from "../../auth/AuthContext"; export default function MapView({ theme = "dark" }) { const { authenticated, username, token } = useAuth(); const { containerRef, mapRef, markersRef, expandedElRef, viewParams, hasLastView, } = useMapCore(theme); 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]); // WebSocket live 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 { // ignore } }; 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 handleOpenCreate = () => { if (!authenticated) { alert("You must be logged in to place a wire."); return; } 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; if (!authenticated) { setSaveError("You must be logged in to post."); return; } const authorName = (username || "").trim() || "anon"; // Crosshair offset fix let baseLng; let baseLat; if (draftCoords && draftCoords.length === 2) { baseLng = draftCoords[0]; baseLat = draftCoords[1]; } else { const center = map.getCenter(); baseLng = center.lng; baseLat = center.lat; } const pixelOffsetY = -20; const screenPoint = map.project([baseLng, baseLat]); const correctedPoint = { x: screenPoint.x, y: screenPoint.y + pixelOffsetY, }; const correctedLngLat = map.unproject([correctedPoint.x, correctedPoint.y]); const lng = correctedLngLat.lng; const lat = correctedLngLat.lat; if (!draftTitle.trim()) { setSaveError("Title required."); return; } try { setIsSaving(true); const newPost = await createPost( { title: draftTitle.trim(), snippet: draftBody.trim() || draftTitle.trim(), category: draftCategory.toUpperCase(), sub_category: draftSubCategory || "ALL", lat, lon: lng, author: authorName, }, token ); if (newPost && newPost.id) { handleIncomingPost(newPost); } setIsSaving(false); setIsCreating(false); } catch (e) { console.error(e); 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 (typeof lng === "number" && typeof lat === "number") { map.flyTo({ center: [lng, lat], zoom: 8, speed: 1.4 }); } }; return (
{status &&
{status}
} {/* TOP: Look / Place */}
{/* LEFT: time filters – NOW / TODAY / RECENT / PAST */}
{[ ["NOW", "Now"], ["TODAY", "Today"], ["RECENT", "Recent"], ["PAST", "Past"], ].map(([code, label]) => (
{label}
))}
{/* RIGHT: main categories – ALL / NEWS / FRIENDS / EVENTS / MARKET */}
{FILTER_MAIN_CATEGORIES.map((cat) => { const label = cat; return (
{label}
); })}
{/* BOTTOM: subcategories */}
{bottomCategories.map((c) => ( ))}
{/* MY SPOT */}
{/* CROSSHAIR */} {isCreating && (
)} {/* CREATE POST PANEL */} {isCreating && (
Create wire
Move the map, aim with the crosshair β€” your wire will be pinned there.
setDraftTitle(e.target.value)} />