This commit is contained in:
Your Name 2025-12-20 22:21:59 -05:00
parent 1b52075561
commit 5bffb63028
2 changed files with 72 additions and 26 deletions

View File

@ -44,7 +44,9 @@ export default function MapView({
const [subFilter, setSubFilter] = useState("All"); const [subFilter, setSubFilter] = useState("All");
useEffect(() => { useEffect(() => {
try { localStorage.setItem(TIME_FILTER_KEY, timeFilter); } catch {} try {
localStorage.setItem(TIME_FILTER_KEY, timeFilter);
} catch {}
}, [timeFilter]); }, [timeFilter]);
const stageRef = useRef(null); const stageRef = useRef(null);
@ -52,6 +54,8 @@ export default function MapView({
const userPosition = useUserPosition(mapRef, hasLastView); const userPosition = useUserPosition(mapRef, hasLastView);
const [searchQuery, setSearchQuery] = useState("");
const { status, visiblePosts, loadingPosts, loadError, handleIncomingPost } = usePostsEngine({ const { status, visiblePosts, loadingPosts, loadError, handleIncomingPost } = usePostsEngine({
theme, theme,
mapRef, mapRef,
@ -59,6 +63,7 @@ export default function MapView({
mainFilter, mainFilter,
subFilter, subFilter,
timeFilter, timeFilter,
searchQuery,
markersRef, markersRef,
expandedElRef, expandedElRef,
}); });
@ -72,8 +77,6 @@ export default function MapView({
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState(""); const [saveError, setSaveError] = useState("");
const [searchQuery, setSearchQuery] = useState("");
const bottomCategories = FILTER_CATEGORY_MAP[mainFilter] || ["All"]; const bottomCategories = FILTER_CATEGORY_MAP[mainFilter] || ["All"];
/* SW_SUBCAT_ICONS:BEGIN */ /* SW_SUBCAT_ICONS:BEGIN */
@ -155,7 +158,7 @@ export default function MapView({
} }
}, [selectedPost, mapRef]); }, [selectedPost, mapRef]);
// Websocket for new posts // Hide subcats when map isn't visible enough (keeps UI clean)
useEffect(() => { useEffect(() => {
const el = stageRef.current; const el = stageRef.current;
if (!el || typeof IntersectionObserver === "undefined") return; if (!el || typeof IntersectionObserver === "undefined") return;
@ -173,6 +176,7 @@ export default function MapView({
return () => obs.disconnect(); return () => obs.disconnect();
}, []); }, []);
// Websocket for new posts
useEffect(() => { useEffect(() => {
const proto = window.location.protocol === "https:" ? "wss" : "ws"; const proto = window.location.protocol === "https:" ? "wss" : "ws";
const host = const host =
@ -211,9 +215,12 @@ export default function MapView({
} }
if (needsEmailVerification) { if (needsEmailVerification) {
alert("You must verify your email before posting. Use 'Resend email' in the top bar, then login again."); alert(
"You must verify your email before posting. Use 'Resend email' in the top bar, then login again."
);
return; return;
} }
setIsCreating(true); setIsCreating(true);
setDraftTitle(""); setDraftTitle("");
setDraftBody(""); setDraftBody("");
@ -224,11 +231,9 @@ setIsCreating(true);
setDraftCoords(null); setDraftCoords(null);
}; };
// Keep submit to allow Enter key, but no popup:
const handleSearchSubmit = (e) => { const handleSearchSubmit = (e) => {
e.preventDefault(); e.preventDefault();
const q = (searchQuery || "").trim();
if (!q) return;
alert(`🔍 Search: ${q} (coming soon!)`);
}; };
const handleClearSearch = () => { const handleClearSearch = () => {
@ -247,11 +252,11 @@ setIsCreating(true);
return; return;
} }
if (needsEmailVerification) { if (needsEmailVerification) {
setSaveError("Verify your email to post. Use 'Resend email' in the top bar, then login again."); setSaveError("Verify your email to post. Use 'Resend email' in the top bar, then login again.");
return; return;
} }
const authorName = (username || "").trim() || "anon"; const authorName = (username || "").trim() || "anon";
let baseLng; let baseLng;
@ -305,7 +310,9 @@ const authorName = (username || "").trim() || "anon";
const code = e && (e.code || ""); const code = e && (e.code || "");
const msg = String((e && e.message) || "").toLowerCase(); const msg = String((e && e.message) || "").toLowerCase();
const isNotVerified = code === "email_not_verified" || (e && e.status === 403 && (msg.includes("not verified") || msg.includes("verify"))); const isNotVerified =
code === "email_not_verified" ||
(e && e.status === 403 && (msg.includes("not verified") || msg.includes("verify")));
if (isNotVerified) { if (isNotVerified) {
setSaveError("Verify your email to post. Use 'Resend email' in the top bar, then login again."); setSaveError("Verify your email to post. Use 'Resend email' in the top bar, then login again.");
@ -339,7 +346,12 @@ const authorName = (username || "").trim() || "anon";
{status && <div className="map-status">{status}</div>} {status && <div className="map-status">{status}</div>}
<div className="map-overlay map-overlay-top"> <div className="map-overlay map-overlay-top">
<form className="sw-searchbar" onSubmit={handleSearchSubmit} role="search" aria-label="Search"> <form
className="sw-searchbar"
onSubmit={handleSearchSubmit}
role="search"
aria-label="Search"
>
<i className="fa-solid fa-magnifying-glass sw-search-icon" /> <i className="fa-solid fa-magnifying-glass sw-search-icon" />
<input <input
className="sw-search-input" className="sw-search-input"

View File

@ -2,8 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { fetchPosts } from "../../api/client"; import { fetchPosts } from "../../api/client";
import { categoryCode, matchesSubFilter, matchesTimeFilter } from "./mapFilter"; import { categoryCode, matchesSubFilter, matchesTimeFilter } from "./mapFilter";
import { haversineKm } from "./mapGeo"; import { haversineKm } from "./mapGeo";
import { createMarkerForPost, clearAllMarkers } from "./markerManager"; import { createMarkerForPost } from "./markerManager";
function getId(p) { function getId(p) {
return p?.id ?? p?._id ?? null; return p?.id ?? p?._id ?? null;
@ -19,12 +18,48 @@ function sameIdList(a, b) {
return true; return true;
} }
function asText(v) {
if (v == null) return "";
return String(v);
}
function postHaystack(p) {
const title = asText(p?.title || p?.text || p?.body || "");
const snippet = asText(p?.snippet || p?.summary || p?.content || "");
const author = asText(p?.author || p?.Author || p?.user || p?.username || "");
const city = asText(p?.city || p?.location_name || p?.place || "");
const cat = asText(p?.category || p?.Category || "");
const sub = asText(p?.sub_category || p?.subCategory || "");
return `${title} ${snippet} ${author} ${city} ${cat} ${sub}`.toLowerCase();
}
function matchesSearch(p, qRaw) {
const q = (qRaw || "").toString().trim().toLowerCase();
if (!q) return true;
const author = asText(p?.author || p?.Author || p?.user || p?.username || "").toLowerCase();
const hay = postHaystack(p);
const terms = q.split(/\s+/).map((t) => t.trim()).filter(Boolean);
for (const t of terms) {
if (t.startsWith("@")) {
const want = t.slice(1).trim();
if (!want) continue;
if (!author.includes(want)) return false;
continue;
}
if (!hay.includes(t)) return false;
}
return true;
}
export function usePostsEngine({ export function usePostsEngine({
mapRef, mapRef,
viewParams, viewParams,
mainFilter, mainFilter,
subFilter, subFilter,
timeFilter, timeFilter,
searchQuery,
markersRef, markersRef,
expandedElRef, expandedElRef,
theme = "blue", theme = "blue",
@ -103,10 +138,11 @@ export function usePostsEngine({
} }
if (!matchesSubFilter(p, subFilter)) return false; if (!matchesSubFilter(p, subFilter)) return false;
if (!matchesTimeFilter(p.created_at || p.CreatedAt || p.createdAt, tf)) return false; if (!matchesTimeFilter(p.created_at || p.CreatedAt || p.createdAt, tf)) return false;
if (!matchesSearch(p, searchQuery)) return false;
return true; return true;
}); });
}, },
[mainFilter, subFilter] [mainFilter, subFilter, searchQuery]
); );
const refreshVisibleAndMarkers = useCallback( const refreshVisibleAndMarkers = useCallback(
@ -116,7 +152,7 @@ export function usePostsEngine({
// reduce wall blink: don't update if same ids // reduce wall blink: don't update if same ids
setVisiblePosts((prev) => (sameIdList(prev, v) ? prev : v)); setVisiblePosts((prev) => (sameIdList(prev, v) ? prev : v));
// no annoying "No posts found" bubble // keep status silent (no annoying bubbles)
setStatus(""); setStatus("");
syncMarkers(v); syncMarkers(v);
@ -124,10 +160,10 @@ export function usePostsEngine({
[computeVisible, syncMarkers] [computeVisible, syncMarkers]
); );
// On filter changes: recompute + sync (NO clear-all rebuild) // On filter/search changes: recompute + sync (NO clear-all rebuild)
useEffect(() => { useEffect(() => {
refreshVisibleAndMarkers(timeFilter); refreshVisibleAndMarkers(timeFilter);
}, [timeFilter, mainFilter, subFilter, refreshVisibleAndMarkers]); }, [timeFilter, mainFilter, subFilter, searchQuery, refreshVisibleAndMarkers]);
// Fetch on view change (moveend) // Fetch on view change (moveend)
useEffect(() => { useEffect(() => {
@ -245,6 +281,7 @@ export function usePostsEngine({
} }
if (!matchesSubFilter(p, subFilter)) return; if (!matchesSubFilter(p, subFilter)) return;
if (!matchesTimeFilter(p.created_at || p.CreatedAt || p.createdAt, timeFilter)) return; if (!matchesTimeFilter(p.created_at || p.CreatedAt || p.createdAt, timeFilter)) return;
if (!matchesSearch(p, searchQuery)) return;
// add marker if missing // add marker if missing
const id = getId(p); const id = getId(p);
@ -253,12 +290,9 @@ export function usePostsEngine({
createMarkerForPost(p, mapRef, markersRef, expandedElRef, theme); createMarkerForPost(p, mapRef, markersRef, expandedElRef, theme);
} }
setVisiblePosts((current) => { setVisiblePosts((current) => [...current, p]);
const next = [...current, p];
return next;
});
}, },
[mainFilter, subFilter, timeFilter, mapRef, markersRef, expandedElRef, theme] [mainFilter, subFilter, timeFilter, searchQuery, mapRef, markersRef, expandedElRef, theme]
); );
return { status, visiblePosts, loadingPosts, loadError, handleIncomingPost }; return { status, visiblePosts, loadingPosts, loadError, handleIncomingPost };