import React, { useEffect, useRef, useState } from "react"; import "maplibre-gl/dist/maplibre-gl.css"; import "../../styles/map.css"; import "../../styles/mapMarkers.css"; import "../../styles/posts.css"; import "../../styles/filters.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 { useAuth } from "../../auth/AuthContext"; import FilterButtons from "../Filters/FilterButtons"; import TimeFilterButtons from "../Filters/TimeFilterButtons"; export default function MapView({ theme = "dark", onPostsState, selectedPost, onSelectPost, }) { const { authenticated, username, token, needsEmailVerification } = useAuth(); const { containerRef, mapRef, markersRef, expandedElRef, viewParams, hasLastView } = useMapCore(theme); const [mainFilter, setMainFilter] = useState("All"); const TIME_FILTER_KEY = "sociowire:timeFilter"; const [timeFilter, setTimeFilter] = useState(() => { try { const v = localStorage.getItem(TIME_FILTER_KEY); const ok = ["NOW", "TODAY", "RECENT", "PAST"].includes(v); return ok ? v : "TODAY"; } catch { return "TODAY"; } }); const [subFilter, setSubFilter] = useState("All"); useEffect(() => { try { localStorage.setItem(TIME_FILTER_KEY, timeFilter); } catch {} }, [timeFilter]); const stageRef = useRef(null); const [showSubcats, setShowSubcats] = useState(true); const userPosition = useUserPosition(mapRef, hasLastView); const [searchQuery, setSearchQuery] = useState(""); const { status, visiblePosts, loadingPosts, loadError, handleIncomingPost } = usePostsEngine({ theme, mapRef, viewParams, mainFilter, subFilter, timeFilter, searchQuery, markersRef, expandedElRef, }); 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 [draftImage, setDraftImage] = useState(""); const [draftImageFile, setDraftImageFile] = useState(null); const [draftImagePreview, setDraftImagePreview] = useState(""); const [useCustomImage, setUseCustomImage] = useState(false); const [uploadingImage, setUploadingImage] = useState(false); const [isSaving, setIsSaving] = useState(false); const [saveError, setSaveError] = useState(""); const bottomCategories = FILTER_CATEGORY_MAP[mainFilter] || ["All"]; const getSubcatIcon = (main, sub) => { const M = { All: { All: "fa-solid fa-layer-group", }, News: { All: "fa-solid fa-layer-group", World: "fa-solid fa-globe", Politics: "fa-solid fa-landmark-flag", Tech: "fa-solid fa-microchip", Finance: "fa-solid fa-chart-line", Local: "fa-solid fa-location-dot", }, Friends: { All: "fa-solid fa-layer-group", Nearby: "fa-solid fa-location-crosshairs", Chats: "fa-solid fa-comments", Groups: "fa-solid fa-user-group", Stories: "fa-solid fa-book-open", Favs: "fa-solid fa-heart", }, Events: { All: "fa-solid fa-layer-group", Today: "fa-solid fa-calendar-day", Weekend: "fa-solid fa-calendar-week", Music: "fa-solid fa-music", Sports: "fa-solid fa-football", Local: "fa-solid fa-location-dot", }, Market: { All: "fa-solid fa-layer-group", Actu: "fa-solid fa-newspaper", Tech: "fa-solid fa-microchip", Finan: "fa-solid fa-money-bill-trend-up", Sport: "fa-solid fa-basketball", Deals: "fa-solid fa-tags", }, }; return (M[main] && M[main][sub]) || ""; }; useEffect(() => { if (!bottomCategories.length) return; if (!subFilter || !bottomCategories.includes(subFilter)) setSubFilter("All"); }, [mainFilter, bottomCategories, subFilter]); useEffect(() => { if (!onPostsState) return; onPostsState({ status, posts: visiblePosts, loading: loadingPosts, error: loadError, }); }, [status, visiblePosts, loadingPosts, loadError, onPostsState]); useEffect(() => { if (!selectedPost) return; const map = mapRef.current; if (!map) return; const lng = selectedPost.lon ?? selectedPost.lng; const lat = selectedPost.lat ?? selectedPost.latitude; if (typeof lng === "number" && typeof lat === "number") { map.flyTo({ center: [lng, lat], zoom: 8, speed: 1.4 }); } }, [selectedPost, mapRef]); useEffect(() => { const el = stageRef.current; if (!el || typeof IntersectionObserver === "undefined") return; const obs = new IntersectionObserver( (entries) => { const e = entries && entries[0]; const ok = !!(e && e.isIntersecting && e.intersectionRatio > 0.15); setShowSubcats(ok); }, { threshold: [0, 0.15, 0.3, 0.6] } ); obs.observe(el); return () => obs.disconnect(); }, []); 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 handleOpenCreate = () => { if (!authenticated) { alert("You must be logged in to place a wire."); return; } if (needsEmailVerification) { alert( "You must verify your email before posting. Use 'Resend email' in the top bar, then login again." ); return; } setIsCreating(true); setDraftTitle(""); setDraftBody(""); setDraftImage(""); setDraftImageFile(null); setDraftImagePreview(""); setUseCustomImage(false); const main = mainFilter === "All" ? "News" : mainFilter; setDraftCategory(main); const list = CREATE_CATEGORY_MAP[main] || CREATE_CATEGORY_MAP.News; setDraftSubCategory(list[0]); setDraftCoords(null); }; const handleSearchSubmit = (e) => { e.preventDefault(); }; const handleClearSearch = () => { setSearchQuery(""); }; const handleImageFileChange = async (e) => { const file = e.target.files?.[0]; if (!file) return; // Vérifier le type de fichier if (!file.type.startsWith('image/')) { setSaveError("Please select an image file"); return; } // Vérifier la taille (max 5MB) if (file.size > 5 * 1024 * 1024) { setSaveError("Image must be less than 5MB"); return; } setDraftImageFile(file); // Créer preview const reader = new FileReader(); reader.onloadend = () => { setDraftImagePreview(reader.result); }; reader.readAsDataURL(file); // Upload immédiatement setUploadingImage(true); setSaveError(""); try { const formData = new FormData(); formData.append('image', file); const res = await fetch('/api/upload-image', { method: 'POST', body: formData, credentials: 'include', headers: token ? { 'Authorization': `Bearer ${token}` } : {}, }); if (!res.ok) { throw new Error('Upload failed'); } const data = await res.json(); setDraftImage(data.url || data.image_url); setUploadingImage(false); } catch (err) { console.error('Image upload error:', err); setSaveError('Failed to upload image'); setUploadingImage(false); setDraftImageFile(null); setDraftImagePreview(""); } }; 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; } if (needsEmailVerification) { setSaveError( "Verify your email to post. Use 'Resend email' in the top bar, then login again." ); return; } const authorName = (username || "").trim() || "anon"; 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 payload = { title: draftTitle.trim(), snippet: draftBody.trim() || draftTitle.trim(), category: draftCategory === "Events" ? "EVENT" : draftCategory.toUpperCase(), sub_category: draftSubCategory || "ALL", lat, lon: lng, author: authorName, }; if (useCustomImage && draftImage.trim()) { payload.image_small = draftImage.trim(); payload.image_large = draftImage.trim(); } const newPost = await createPost(payload, token); if (newPost && newPost.id) handleIncomingPost(newPost); setIsSaving(false); setIsCreating(false); } catch (e) { console.error(e); setIsSaving(false); const code = e && (e.code || ""); const msg = String((e && e.message) || "").toLowerCase(); const isNotVerified = code === "email_not_verified" || (e && e.status === 403 && (msg.includes("not verified") || msg.includes("verify"))); if (isNotVerified) { setSaveError( "Verify your email to post. Use 'Resend email' in the top bar, then login again." ); } else { setSaveError((e && e.message) || "Error creating post."); } } }; const handleMainFilterSelect = (code) => { if (!FILTER_MAIN_CATEGORIES.includes(code)) return; setMainFilter(code); }; const handleSelectPost = (post) => { if (onSelectPost) onSelectPost(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: Search à gauche, 2 icônes à droite, sur une seule ligne */}
setSearchQuery(e.target.value)} autoCapitalize="none" autoCorrect="off" spellCheck={false} /> {searchQuery ? ( ) : null}
setTimeFilter(code)} />
{bottomCategories.map((c) => ( ))}
{isCreating && (
)} {isCreating && (
Create wire
Move the map, aim with the crosshair — your wire will be pinned there.
setDraftTitle(e.target.value)} />