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");
useEffect(() => {
try { localStorage.setItem(TIME_FILTER_KEY, timeFilter); } catch {}
try {
localStorage.setItem(TIME_FILTER_KEY, timeFilter);
} catch {}
}, [timeFilter]);
const stageRef = useRef(null);
@ -52,6 +54,8 @@ export default function MapView({
const userPosition = useUserPosition(mapRef, hasLastView);
const [searchQuery, setSearchQuery] = useState("");
const { status, visiblePosts, loadingPosts, loadError, handleIncomingPost } = usePostsEngine({
theme,
mapRef,
@ -59,6 +63,7 @@ export default function MapView({
mainFilter,
subFilter,
timeFilter,
searchQuery,
markersRef,
expandedElRef,
});
@ -72,8 +77,6 @@ export default function MapView({
const [isSaving, setIsSaving] = useState(false);
const [saveError, setSaveError] = useState("");
const [searchQuery, setSearchQuery] = useState("");
const bottomCategories = FILTER_CATEGORY_MAP[mainFilter] || ["All"];
/* SW_SUBCAT_ICONS:BEGIN */
@ -155,7 +158,7 @@ export default function MapView({
}
}, [selectedPost, mapRef]);
// Websocket for new posts
// Hide subcats when map isn't visible enough (keeps UI clean)
useEffect(() => {
const el = stageRef.current;
if (!el || typeof IntersectionObserver === "undefined") return;
@ -173,6 +176,7 @@ export default function MapView({
return () => obs.disconnect();
}, []);
// Websocket for new posts
useEffect(() => {
const proto = window.location.protocol === "https:" ? "wss" : "ws";
const host =
@ -211,9 +215,12 @@ export default function MapView({
}
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;
}
setIsCreating(true);
setDraftTitle("");
setDraftBody("");
@ -224,11 +231,9 @@ setIsCreating(true);
setDraftCoords(null);
};
// Keep submit to allow Enter key, but no popup:
const handleSearchSubmit = (e) => {
e.preventDefault();
const q = (searchQuery || "").trim();
if (!q) return;
alert(`🔍 Search: ${q} (coming soon!)`);
};
const handleClearSearch = () => {
@ -247,11 +252,11 @@ setIsCreating(true);
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;
@ -305,7 +310,9 @@ const authorName = (username || "").trim() || "anon";
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")));
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.");
@ -339,7 +346,12 @@ const authorName = (username || "").trim() || "anon";
{status && <div className="map-status">{status}</div>}
<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" />
<input
className="sw-search-input"

View File

@ -2,8 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react";
import { fetchPosts } from "../../api/client";
import { categoryCode, matchesSubFilter, matchesTimeFilter } from "./mapFilter";
import { haversineKm } from "./mapGeo";
import { createMarkerForPost, clearAllMarkers } from "./markerManager";
import { createMarkerForPost } from "./markerManager";
function getId(p) {
return p?.id ?? p?._id ?? null;
@ -19,12 +18,48 @@ function sameIdList(a, b) {
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({
mapRef,
viewParams,
mainFilter,
subFilter,
timeFilter,
searchQuery,
markersRef,
expandedElRef,
theme = "blue",
@ -103,10 +138,11 @@ export function usePostsEngine({
}
if (!matchesSubFilter(p, subFilter)) return false;
if (!matchesTimeFilter(p.created_at || p.CreatedAt || p.createdAt, tf)) return false;
if (!matchesSearch(p, searchQuery)) return false;
return true;
});
},
[mainFilter, subFilter]
[mainFilter, subFilter, searchQuery]
);
const refreshVisibleAndMarkers = useCallback(
@ -116,7 +152,7 @@ export function usePostsEngine({
// reduce wall blink: don't update if same ids
setVisiblePosts((prev) => (sameIdList(prev, v) ? prev : v));
// no annoying "No posts found" bubble
// keep status silent (no annoying bubbles)
setStatus("");
syncMarkers(v);
@ -124,10 +160,10 @@ export function usePostsEngine({
[computeVisible, syncMarkers]
);
// On filter changes: recompute + sync (NO clear-all rebuild)
// On filter/search changes: recompute + sync (NO clear-all rebuild)
useEffect(() => {
refreshVisibleAndMarkers(timeFilter);
}, [timeFilter, mainFilter, subFilter, refreshVisibleAndMarkers]);
}, [timeFilter, mainFilter, subFilter, searchQuery, refreshVisibleAndMarkers]);
// Fetch on view change (moveend)
useEffect(() => {
@ -245,6 +281,7 @@ export function usePostsEngine({
}
if (!matchesSubFilter(p, subFilter)) return;
if (!matchesTimeFilter(p.created_at || p.CreatedAt || p.createdAt, timeFilter)) return;
if (!matchesSearch(p, searchQuery)) return;
// add marker if missing
const id = getId(p);
@ -253,12 +290,9 @@ export function usePostsEngine({
createMarkerForPost(p, mapRef, markersRef, expandedElRef, theme);
}
setVisiblePosts((current) => {
const next = [...current, p];
return next;
});
setVisiblePosts((current) => [...current, p]);
},
[mainFilter, subFilter, timeFilter, mapRef, markersRef, expandedElRef, theme]
[mainFilter, subFilter, timeFilter, searchQuery, mapRef, markersRef, expandedElRef, theme]
);
return { status, visiblePosts, loadingPosts, loadError, handleIncomingPost };