Switch time filters to hours-based range

This commit is contained in:
Your Name 2025-12-30 19:54:21 -05:00
parent d854da7c1b
commit bd512ba04d
5 changed files with 62 additions and 77 deletions

View File

@ -409,7 +409,7 @@ export async function fetchSmartFeed(params = {}) {
if (typeof params.zoom === "number") qs.set("zoom", String(params.zoom)); if (typeof params.zoom === "number") qs.set("zoom", String(params.zoom));
if (params.category) qs.set("category", String(params.category)); if (params.category) qs.set("category", String(params.category));
if (params.sub_category) qs.set("sub_category", String(params.sub_category)); if (params.sub_category) qs.set("sub_category", String(params.sub_category));
if (params.time_filter) qs.set("time_filter", String(params.time_filter)); if (params.time_hours) qs.set("time_hours", String(params.time_hours));
// NEW: Screen bounds for on-screen prioritization // NEW: Screen bounds for on-screen prioritization
if (params.bounds) { if (params.bounds) {

View File

@ -1,21 +1,21 @@
import React from "react"; import React from "react";
export default function TimeFilterButtons({ active, onSelect }) { export default function TimeFilterButtons({ activeHours, onSelect }) {
const buttons = [ const buttons = [
{ code: "NOW", label: "Now", icon: "fa-solid fa-bolt" }, { hours: 2, label: "Now", icon: "fa-solid fa-bolt" },
{ code: "TODAY", label: "Today", icon: "fa-solid fa-sun" }, { hours: 24, label: "Today", icon: "fa-solid fa-sun" },
{ code: "RECENT", label: "Recent", icon: "fa-solid fa-clock-rotate-left" }, { hours: 72, label: "Recent", icon: "fa-solid fa-clock-rotate-left" },
{ code: "PAST", label: "Old", icon: "fa-solid fa-hourglass-half" }, { hours: 336, label: "Old", icon: "fa-solid fa-hourglass-half" },
]; ];
return ( return (
<div className="sw-icon-column"> <div className="sw-icon-column">
{buttons.map((btn) => ( {buttons.map((btn) => (
<button <button
key={btn.code} key={btn.hours}
type="button" type="button"
className={"sw-icon-btn" + (active === btn.code ? " sw-icon-active" : "")} className={"sw-icon-btn" + (activeHours === btn.hours ? " sw-icon-active" : "")}
onClick={() => onSelect && onSelect(btn.code)} onClick={() => onSelect && onSelect(btn.hours)}
> >
<div className="sw-icon-circle"> <div className="sw-icon-circle">
<i className={btn.icon} /> <i className={btn.icon} />

View File

@ -40,14 +40,14 @@ export default function MapView({
useMapCore(theme); useMapCore(theme);
const [mainFilter, setMainFilter] = useState("All"); const [mainFilter, setMainFilter] = useState("All");
const TIME_FILTER_KEY = "sociowire:timeFilter"; const TIME_FILTER_KEY = "sociowire:timeHours";
const [timeFilter, setTimeFilter] = useState(() => { const [timeHours, setTimeHours] = useState(() => {
try { try {
const v = localStorage.getItem(TIME_FILTER_KEY); const v = localStorage.getItem(TIME_FILTER_KEY);
const ok = ["NOW", "TODAY", "RECENT", "PAST"].includes(v); const n = Number(v);
return ok ? v : "PAST"; return Number.isFinite(n) && n > 0 ? n : 336;
} catch { } catch {
return "PAST"; return 336;
} }
}); });
const [subFilter, setSubFilter] = useState("All"); const [subFilter, setSubFilter] = useState("All");
@ -55,9 +55,9 @@ export default function MapView({
useEffect(() => { useEffect(() => {
try { try {
localStorage.setItem(TIME_FILTER_KEY, timeFilter); localStorage.setItem(TIME_FILTER_KEY, String(timeHours));
} catch {} } catch {}
}, [timeFilter]); }, [timeHours]);
const stageRef = useRef(null); const stageRef = useRef(null);
const [showSubcats, setShowSubcats] = useState(true); const [showSubcats, setShowSubcats] = useState(true);
@ -159,7 +159,7 @@ export default function MapView({
viewParams, viewParams,
mainFilter, mainFilter,
subFilter, subFilter,
timeFilter, timeHours,
searchQuery, searchQuery,
clusterFocus, clusterFocus,
mainNewsOnly, mainNewsOnly,
@ -168,7 +168,7 @@ export default function MapView({
forceFullPostId, forceFullPostId,
markersRef, markersRef,
expandedElRef, expandedElRef,
onAutoWidenTimeFilter: (next) => setTimeFilter(next), onAutoWidenTimeFilter: () => setTimeHours(336),
onSelectPost, onSelectPost,
username, username,
}); });
@ -312,8 +312,8 @@ export default function MapView({
}, [subFilter, mainFilter]); }, [subFilter, mainFilter]);
useEffect(() => { useEffect(() => {
trackEvent("filter_time_change", { filter: timeFilter }); trackEvent("filter_time_change", { hours: timeHours });
}, [timeFilter]); }, [timeHours]);
useEffect(() => { useEffect(() => {
if (queryPostRef.current) return; if (queryPostRef.current) return;
@ -627,7 +627,7 @@ export default function MapView({
setSearchQuery(""); setSearchQuery("");
setMainFilter("All"); setMainFilter("All");
setSubFilter("All"); setSubFilter("All");
setTimeFilter("PAST"); // Show all time periods setTimeHours(336); // 2 weeks
const postId = Number(raw?.post_id ?? raw?.id ?? item?.id ?? 0); const postId = Number(raw?.post_id ?? raw?.id ?? item?.id ?? 0);
const rawUrl = raw?.url || item?.url || ""; const rawUrl = raw?.url || item?.url || "";
@ -748,7 +748,7 @@ export default function MapView({
if (q) { if (q) {
setMainFilter("All"); setMainFilter("All");
setSubFilter("All"); setSubFilter("All");
setTimeFilter("PAST"); setTimeHours(336);
} }
}; };
@ -762,7 +762,7 @@ export default function MapView({
if (query.trim()) { if (query.trim()) {
setMainFilter("All"); setMainFilter("All");
setSubFilter("All"); setSubFilter("All");
setTimeFilter("PAST"); // Show all time periods setTimeHours(336); // 2 weeks
} }
}; };
@ -1254,8 +1254,8 @@ export default function MapView({
<div className="map-overlay map-overlay-left"> <div className="map-overlay map-overlay-left">
<TimeFilterButtons <TimeFilterButtons
active={timeFilter} activeHours={timeHours}
onSelect={(code) => setTimeFilter(code)} onSelect={(hours) => setTimeHours(hours)}
/> />
</div> </div>

View File

@ -54,14 +54,12 @@ function parseCreatedAt(createdAt) {
return Number.isNaN(d.getTime()) ? null : d; return Number.isNaN(d.getTime()) ? null : d;
} }
export function matchesTimeFilter(createdAt, timeFilter) { export function matchesTimeFilter(createdAt, timeHours) {
if (!timeFilter) return true; const hours = Number(timeHours);
if (timeFilter === "PAST") return true; if (!Number.isFinite(hours) || hours <= 0) return true;
const d = parseCreatedAt(createdAt); const d = parseCreatedAt(createdAt);
if (!d) { if (!d) return false;
return timeFilter !== "NOW";
}
const now = Date.now(); const now = Date.now();
const diffMs = now - d.getTime(); const diffMs = now - d.getTime();
@ -69,20 +67,7 @@ export function matchesTimeFilter(createdAt, timeFilter) {
if (diffMs < 0) return false; if (diffMs < 0) return false;
const diffHours = diffMs / 3600000; const diffHours = diffMs / 3600000;
const diffDays = diffMs / 86400000; return diffHours <= hours;
switch (timeFilter) {
case "NOW":
return diffHours <= 2;
case "TODAY":
return diffHours <= 24;
case "RECENT":
return diffHours <= 72;
case "PAST":
return diffDays <= 15;
default:
return true;
}
} }
export function effectivePostTime(post) { export function effectivePostTime(post) {

View File

@ -360,7 +360,7 @@ export function usePostsEngine({
viewParams, viewParams,
mainFilter, mainFilter,
subFilter, subFilter,
timeFilter, timeHours,
searchQuery, searchQuery,
clusterFocus, clusterFocus,
mainNewsOnly, mainNewsOnly,
@ -375,7 +375,7 @@ export function usePostsEngine({
theme = "blue", theme = "blue",
}) { }) {
const allPostsRef = useRef([]); const allPostsRef = useRef([]);
const lastFetchRef = useRef({ center: null, radiusKm: null, filterKey: "", timeFilter: "", bounds: null }); const lastFetchRef = useRef({ center: null, radiusKm: null, filterKey: "", timeHours: 0, bounds: null });
const pendingFilterRef = useRef(""); const pendingFilterRef = useRef("");
const requestSeqRef = useRef(0); const requestSeqRef = useRef(0);
const delayedFetchRef = useRef(null); const delayedFetchRef = useRef(null);
@ -433,7 +433,7 @@ export function usePostsEngine({
useEffect(() => { useEffect(() => {
const map = mapRef.current; const map = mapRef.current;
replacePoolRef.current = true; replacePoolRef.current = true;
pendingFilterRef.current = `${mainFilter}|${subFilter}|${timeFilter}`; pendingFilterRef.current = `${mainFilter}|${subFilter}|${timeHours}`;
if (map) { if (map) {
map.__swForceFetchAt = Date.now(); map.__swForceFetchAt = Date.now();
setTimeout(() => { setTimeout(() => {
@ -442,7 +442,7 @@ export function usePostsEngine({
} catch {} } catch {}
}, 60); }, 60);
} }
}, [mainFilter, subFilter, timeFilter, mapRef]); }, [mainFilter, subFilter, timeHours, mapRef]);
const priorityOpts = { const priorityOpts = {
maxFullCards: 15, maxFullCards: 15,
@ -861,7 +861,7 @@ export function usePostsEngine({
if (pc !== catCode) return false; if (pc !== catCode) return false;
} }
if (!matchesSubFilter(p, subFilter)) return false; if (!matchesSubFilter(p, subFilter)) return false;
const skipTimeFilter = lastFetchRef.current?.timeFilter === tf; const skipTimeFilter = lastFetchRef.current?.timeHours === tf;
if (!skipTimeFilter && !matchesTimeFilter(effectivePostTime(p), tf)) return false; if (!skipTimeFilter && !matchesTimeFilter(effectivePostTime(p), tf)) return false;
if (!searchResultsRef.current && !matchesSearch(p, searchQuery)) return false; if (!searchResultsRef.current && !matchesSearch(p, searchQuery)) return false;
return true; return true;
@ -932,8 +932,8 @@ export function usePostsEngine({
// On filter/search changes: recompute + sync (NO clear-all rebuild) // On filter/search changes: recompute + sync (NO clear-all rebuild)
useEffect(() => { useEffect(() => {
if (pendingFilterRef.current) return; if (pendingFilterRef.current) return;
refreshVisibleAndMarkers(timeFilter); refreshVisibleAndMarkers(timeHours);
}, [timeFilter, mainFilter, subFilter, searchQuery, clusterFocus, refreshVisibleAndMarkers]); }, [timeHours, mainFilter, subFilter, searchQuery, clusterFocus, refreshVisibleAndMarkers]);
useEffect(() => { useEffect(() => {
if (!clustersEnabled) return; if (!clustersEnabled) return;
@ -941,14 +941,14 @@ export function usePostsEngine({
if (Array.isArray(clusterFocus.posts) && clusterFocus.posts.length > 0) { if (Array.isArray(clusterFocus.posts) && clusterFocus.posts.length > 0) {
allPostsRef.current = clusterFocus.posts; allPostsRef.current = clusterFocus.posts;
} }
refreshVisibleAndMarkers(timeFilter, true); refreshVisibleAndMarkers(timeHours, true);
}, [clusterFocus, timeFilter, refreshVisibleAndMarkers, clustersEnabled]); }, [clusterFocus, timeHours, refreshVisibleAndMarkers, clustersEnabled]);
// On view change: re-filter markers/cards to current bounds even without fetch // On view change: re-filter markers/cards to current bounds even without fetch
useEffect(() => { useEffect(() => {
if (pendingFilterRef.current) return; if (pendingFilterRef.current) return;
refreshVisibleAndMarkers(timeFilter); refreshVisibleAndMarkers(timeHours);
}, [viewParams, refreshVisibleAndMarkers, timeFilter]); }, [viewParams, refreshVisibleAndMarkers, timeHours]);
// Fetch on view change (moveend) // Fetch on view change (moveend)
useEffect(() => { useEffect(() => {
@ -985,7 +985,7 @@ export function usePostsEngine({
const [lng, lat] = resolvedView.center; const [lng, lat] = resolvedView.center;
const radiusKm = resolvedView.radiusKm || 750; const radiusKm = resolvedView.radiusKm || 750;
const filterKey = `${mainFilter}|${subFilter}`; const filterKey = `${mainFilter}|${subFilter}`;
const currentKey = `${filterKey}|${timeFilter}`; const currentKey = `${filterKey}|${timeHours}`;
const reqId = ++requestSeqRef.current; const reqId = ++requestSeqRef.current;
let bounds = null; let bounds = null;
try { try {
@ -1009,7 +1009,7 @@ export function usePostsEngine({
mapObj.__swForceFetchAt = 0; mapObj.__swForceFetchAt = 0;
} }
if (!forceFetch && last.center && last.filterKey === filterKey && last.timeFilter === timeFilter) { if (!forceFetch && last.center && last.filterKey === filterKey && last.timeHours === timeHours) {
const [lastLng, lastLat] = last.center; const [lastLng, lastLat] = last.center;
const distKm = haversineKm(lastLat, lastLng, lat, lng); const distKm = haversineKm(lastLat, lastLng, lat, lng);
@ -1061,7 +1061,7 @@ export function usePostsEngine({
zoom, zoom,
bounds, bounds,
username: username || undefined, username: username || undefined,
time_filter: timeFilter || undefined, time_hours: timeHours || undefined,
category: catCode || undefined, category: catCode || undefined,
sub_category: subCatParam || undefined, sub_category: subCatParam || undefined,
}); });
@ -1080,7 +1080,7 @@ export function usePostsEngine({
const normalized = normalizePostId(p); const normalized = normalizePostId(p);
const key = getPostKey(normalized); const key = getPostKey(normalized);
if (!key) continue; if (!key) continue;
if (timeFilter) normalized._swTimeFilter = timeFilter; if (timeHours) normalized._swTimeHours = timeHours;
const prior = byId.get(key); const prior = byId.get(key);
if (prior) { if (prior) {
byId.set(key, { ...prior, ...normalized }); byId.set(key, { ...prior, ...normalized });
@ -1092,14 +1092,14 @@ export function usePostsEngine({
allPostsRef.current = prunePosts(Array.from(byId.values()), [lng, lat], MAX_POOL_POSTS, bounds); allPostsRef.current = prunePosts(Array.from(byId.values()), [lng, lat], MAX_POOL_POSTS, bounds);
replacePoolRef.current = false; replacePoolRef.current = false;
lastFetchRef.current = { center: [...resolvedView.center], radiusKm, filterKey, timeFilter, bounds }; lastFetchRef.current = { center: [...resolvedView.center], radiusKm, filterKey, timeHours, bounds };
if (pendingFilterRef.current === currentKey) { if (pendingFilterRef.current === currentKey) {
pendingFilterRef.current = ""; pendingFilterRef.current = "";
} }
// Smooth update: sync markers/clusters + wall without clearing everything // Smooth update: sync markers/clusters + wall without clearing everything
refreshVisibleAndMarkers(timeFilter); refreshVisibleAndMarkers(timeHours);
const visible = computeVisible(timeFilter); const visible = computeVisible(timeHours);
const now = Date.now(); const now = Date.now();
if (now - analyticsDebounceRef.current > 4000) { if (now - analyticsDebounceRef.current > 4000) {
analyticsDebounceRef.current = now; analyticsDebounceRef.current = now;
@ -1108,20 +1108,20 @@ export function usePostsEngine({
radius_km: Math.round(radiusKm || 0), radius_km: Math.round(radiusKm || 0),
zoom: typeof zoom === "number" ? Math.round(zoom * 10) / 10 : undefined, zoom: typeof zoom === "number" ? Math.round(zoom * 10) / 10 : undefined,
filter: filterKey, filter: filterKey,
time_filter: timeFilter || "", time_hours: timeHours || 0,
}); });
} }
if ( if (
!autoWidenRef.current && !autoWidenRef.current &&
timeFilter && timeHours &&
timeFilter !== "PAST" && timeHours > 0 &&
Array.isArray(newPosts) && Array.isArray(newPosts) &&
newPosts.length > 0 && newPosts.length > 0 &&
visible.length === 0 visible.length === 0
) { ) {
autoWidenRef.current = true; autoWidenRef.current = true;
if (typeof onAutoWidenTimeFilter === "function") { if (typeof onAutoWidenTimeFilter === "function") {
onAutoWidenTimeFilter("PAST"); onAutoWidenTimeFilter(336);
} }
} }
@ -1167,7 +1167,7 @@ export function usePostsEngine({
delayedFetchRef.current = null; delayedFetchRef.current = null;
} }
}; };
}, [viewParams, mapReady, mainFilter, subFilter, mapRef, timeFilter, expandedElRef, refreshVisibleAndMarkers, computeVisible, onAutoWidenTimeFilter, syncMarkers, username]); }, [viewParams, mapReady, mainFilter, subFilter, mapRef, timeHours, expandedElRef, refreshVisibleAndMarkers, computeVisible, onAutoWidenTimeFilter, syncMarkers, username]);
useEffect(() => { useEffect(() => {
const q = (searchQuery || "").trim(); const q = (searchQuery || "").trim();
@ -1215,7 +1215,7 @@ export function usePostsEngine({
} catch {} } catch {}
allPostsRef.current = prunePosts(normalized, [lon, lat], MAX_POOL_POSTS, bounds); allPostsRef.current = prunePosts(normalized, [lon, lat], MAX_POOL_POSTS, bounds);
searchResultsRef.current = true; searchResultsRef.current = true;
refreshVisibleAndMarkers(timeFilter); refreshVisibleAndMarkers(timeHours);
trackEvent("search_filter_apply", { trackEvent("search_filter_apply", {
query_len: q.length, query_len: q.length,
results: posts.length, results: posts.length,
@ -1228,7 +1228,7 @@ export function usePostsEngine({
return () => { return () => {
cancelled = true; cancelled = true;
}; };
}, [searchQuery, viewParams, mapRef, refreshVisibleAndMarkers, timeFilter]); }, [searchQuery, viewParams, mapRef, refreshVisibleAndMarkers, timeHours]);
useEffect(() => { useEffect(() => {
if ((searchQuery || "").trim()) return; if ((searchQuery || "").trim()) return;
@ -1243,13 +1243,13 @@ export function usePostsEngine({
if (!clusterModeRef.current) return; if (!clusterModeRef.current) return;
ensureClusterLayers(map); ensureClusterLayers(map);
setClusterVisibility(map, true); setClusterVisibility(map, true);
const { clusterPosts } = splitForClusters(computeVisible(timeFilter)); const { clusterPosts } = splitForClusters(computeVisible(timeHours));
const clusterRemainder = clusterPosts.slice(SIMPLE_MARKER_LIMIT); const clusterRemainder = clusterPosts.slice(SIMPLE_MARKER_LIMIT);
updateClusterData(map, clusterRemainder); updateClusterData(map, clusterRemainder);
}; };
map.on("styledata", handleStyle); map.on("styledata", handleStyle);
return () => map.off("styledata", handleStyle); return () => map.off("styledata", handleStyle);
}, [mapRef, ensureClusterLayers, setClusterVisibility, updateClusterData, computeVisible, timeFilter, splitForClusters, clustersEnabled]); }, [mapRef, ensureClusterLayers, setClusterVisibility, updateClusterData, computeVisible, timeHours, splitForClusters, clustersEnabled]);
const handleIncomingPost = useCallback( const handleIncomingPost = useCallback(
(p) => { (p) => {
@ -1278,11 +1278,11 @@ export function usePostsEngine({
if (pc !== catCode) return; if (pc !== catCode) return;
} }
if (!matchesSubFilter(p, subFilter)) return; if (!matchesSubFilter(p, subFilter)) return;
if (!matchesTimeFilter(effectivePostTime(p), timeFilter)) return; if (!matchesTimeFilter(effectivePostTime(p), timeHours)) return;
if (!matchesSearch(p, searchQuery)) return; if (!matchesSearch(p, searchQuery)) return;
if (clusterModeRef.current) { if (clusterModeRef.current) {
refreshVisibleAndMarkers(timeFilter); refreshVisibleAndMarkers(timeHours);
return; return;
} }
@ -1295,7 +1295,7 @@ export function usePostsEngine({
setVisiblePosts((current) => [normalized, ...current]); setVisiblePosts((current) => [normalized, ...current]);
}, },
[mainFilter, subFilter, timeFilter, searchQuery, mapRef, markersRef, expandedElRef, theme, refreshVisibleAndMarkers] [mainFilter, subFilter, timeHours, searchQuery, mapRef, markersRef, expandedElRef, theme, refreshVisibleAndMarkers]
); );
const handlePostUpdate = useCallback( const handlePostUpdate = useCallback(
@ -1338,7 +1338,7 @@ export function usePostsEngine({
} }
if (changed) { if (changed) {
allPostsRef.current = updated; allPostsRef.current = updated;
refreshVisibleAndMarkers(timeFilter, true); refreshVisibleAndMarkers(timeHours, true);
} }
try { try {
@ -1381,7 +1381,7 @@ export function usePostsEngine({
} }
} catch {} } catch {}
}, },
[mapRef, refreshVisibleAndMarkers, timeFilter] [mapRef, refreshVisibleAndMarkers, timeHours]
); );
return { status, visiblePosts, loadingPosts, loadError, handleIncomingPost, handlePostUpdate }; return { status, visiblePosts, loadingPosts, loadError, handleIncomingPost, handlePostUpdate };