Skip to content

Commit 35d9f02

Browse files
authored
Merge pull request #701 from pharuq411/feat/685-signal-search-filter-discoverability
fix(signals): repair broken filter/feed files and debounce search fil…
2 parents 12d1c9a + a63ceb7 commit 35d9f02

2 files changed

Lines changed: 46 additions & 106 deletions

File tree

components/SignalFeedFilters.tsx

Lines changed: 13 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
"use client";
22

3-
import { useMemo, useRef } from "react";
4-
import { Bookmark, SlidersHorizontal, X } from "lucide-react";
5-
import { useRef, useState } from "react";
6-
import { Bookmark, Save, SlidersHorizontal, Trash2, X } from "lucide-react";
3+
import { useMemo, useRef, useState } from "react";
4+
import { Bookmark, ListFilter, Save, SlidersHorizontal, Trash2, X } from "lucide-react";
75
import { cn } from "@/lib/utils";
86
import type { Signal } from "@/lib/api";
97
import {
@@ -81,6 +79,17 @@ export function SignalFeedFilters({
8179
const assetInputRef = useRef<HTMLInputElement>(null);
8280
const [presetName, setPresetName] = useState("");
8381
const [showPresetInput, setShowPresetInput] = useState(false);
82+
const [savedFiltersOpen, setSavedFiltersOpen] = useState(false);
83+
84+
const counts = useMemo(() => {
85+
if (!signals) return null;
86+
return {
87+
direction: (value: FilterDirection) =>
88+
value === "ALL" ? signals.length : signals.filter((s) => s.action === value).length,
89+
asset: (value: string) => signals.filter((s) => s.asset === value).length,
90+
provider: (value: string) => signals.filter((s) => s.providerId === value).length,
91+
};
92+
}, [signals]);
8493

8594
// Render a neutral placeholder until persisted filters are loaded.
8695
// This prevents filter state from flickering from defaults to saved values.
@@ -106,16 +115,6 @@ export function SignalFeedFilters({
106115
);
107116
}
108117

109-
const counts = useMemo(() => {
110-
if (!signals) return null;
111-
return {
112-
direction: (value: FilterDirection) =>
113-
value === "ALL" ? signals.length : signals.filter((s) => s.action === value).length,
114-
asset: (value: string) => signals.filter((s) => s.asset === value).length,
115-
provider: (value: string) => signals.filter((s) => s.providerId === value).length,
116-
};
117-
}, [signals]);
118-
119118
const isActive =
120119
direction !== "ALL" || asset !== "" || provider !== "" || bookmarkedOnly;
121120

@@ -203,50 +202,6 @@ export function SignalFeedFilters({
203202
{quickProviders.map((providerLabel) => {
204203
const count = counts ? counts.provider(providerLabel) : null;
205204
return (
206-
{quickAssets.map((assetLabel) => (
207-
<button
208-
key={assetLabel}
209-
type="button"
210-
onClick={() => setAsset(asset === assetLabel ? "" : assetLabel)}
211-
aria-pressed={asset === assetLabel}
212-
className={cn(
213-
"rounded-full px-3 py-1 text-xs font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500",
214-
asset === assetLabel
215-
? "bg-emerald-500/15 text-emerald-300 border border-emerald-500/40"
216-
: "bg-surface text-foreground border border-border hover:border-border-strong hover:text-foreground"
217-
)}
218-
>
219-
{assetLabel}
220-
</button>
221-
))}
222-
223-
{quickProviders.map((providerLabel) => (
224-
<button
225-
key={providerLabel}
226-
type="button"
227-
onClick={() =>
228-
setProvider(provider === providerLabel ? "" : providerLabel)
229-
}
230-
aria-pressed={provider === providerLabel}
231-
className={cn(
232-
"rounded-full px-3 py-1 text-xs font-medium transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500",
233-
provider === providerLabel
234-
? "bg-orange-500/15 text-orange-300 border border-orange-500/40"
235-
: "bg-surface text-foreground border border-border hover:border-border-strong hover:text-foreground"
236-
)}
237-
>
238-
{providerLabel}
239-
</button>
240-
))}
241-
</div>
242-
243-
<div className="flex flex-wrap items-center gap-3">
244-
{/* Direction pills */}
245-
<fieldset
246-
className="flex items-center gap-1"
247-
aria-label="Filter by direction"
248-
>
249-
{DIRECTIONS.map(({ label, value }) => (
250205
<button
251206
key={providerLabel}
252207
type="button"

components/signal/SignalFeed.tsx

Lines changed: 33 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,17 @@ import { Search, X, SlidersHorizontal } from "lucide-react";
2424
import { useSyncStatus } from "@/hooks/useSyncStatus";
2525
import { SyncStatusIndicator } from "@/components/SyncStatusIndicator";
2626
import { RelativeTimestamp } from "@/components/RelativeTimestamp";
27-
import { DataPanelError } from "@/components/DataPanelError";
28-
import { classifyError } from "@/hooks/usePanelError";
27+
import { NetworkErrorState } from "@/components/NetworkErrorState";
28+
import { useI18n } from "@/hooks/useI18n";
29+
import { fetchSignals } from "@/lib/api";
30+
import { queryOptions } from "@/lib/queryOptions";
31+
import { usePullToRefresh } from "@/hooks/usePullToRefresh";
32+
import {
33+
readPersistedSplitRatio,
34+
persistSplitRatio,
35+
clampSplitRatio,
36+
computeSplitRatioFromClientX,
37+
} from "@/lib/splitView";
2938

3039
interface SignalResponse {
3140
items: Signal[];
@@ -60,6 +69,10 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
6069
const snoozedMap = useSnoozeStore((state) => state.snoozed);
6170
const pruneExpiredSnoozes = useSnoozeStore((state) => state.pruneExpired);
6271
const [providerSearch, setProviderSearch] = useState(provider);
72+
// #685: debounce the free-text search term used for filtering so a fast
73+
// typist doesn't trigger a full re-filter of the signal list on every
74+
// keystroke. The input itself still updates instantly via providerSearch.
75+
const [debouncedSearch, setDebouncedSearch] = useState(provider);
6376
const [filterSheetOpen, setFilterSheetOpen] = useState(false);
6477
const { addView } = useRecentlyViewedStore();
6578
// Bumped on a timer so expired snoozes are re-evaluated and signals return.
@@ -100,7 +113,7 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
100113
},
101114
getNextPageParam: (lastPage: SignalResponse) => lastPage.nextPage,
102115
initialPageParam: 1,
103-
staleTime: queryOpts.signal.staleTime,
116+
staleTime: queryOptions.signal.staleTime,
104117
placeholderData: (prev) => prev,
105118
// Seed the cache with the server-fetched first page so no client waterfall occurs
106119
...(initialData && {
@@ -116,6 +129,11 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
116129
[data]
117130
);
118131

132+
useEffect(() => {
133+
const handle = setTimeout(() => setDebouncedSearch(providerSearch), 150);
134+
return () => clearTimeout(handle);
135+
}, [providerSearch]);
136+
119137
const availableProviders = useMemo(
120138
() => [...new Set(allSignals.map((s) => s.ticker))].sort(),
121139
[allSignals]
@@ -128,7 +146,7 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
128146

129147
const filteredSignals = useMemo<Signal[]>(() => {
130148
let filtered = [...allSignals];
131-
const searchTerm = providerSearch.trim().toLowerCase();
149+
const searchTerm = debouncedSearch.trim().toLowerCase();
132150

133151
if (direction !== "ALL") {
134152
filtered = filtered.filter((s) => s.action === direction);
@@ -168,7 +186,7 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
168186
direction,
169187
asset,
170188
provider,
171-
providerSearch,
189+
debouncedSearch,
172190
bookmarkedOnly,
173191
bookmarkedIds,
174192
snoozedMap,
@@ -421,8 +439,12 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
421439
<div className="flex flex-col items-end gap-2">
422440
{/* Sort controls — persistent across browsing */}
423441
<SignalSortControls />
424-
{/* Price precision toggle */}
425-
<PricePrecisionToggle />
442+
<div className="flex items-center gap-2">
443+
{/* Price precision toggle */}
444+
<PricePrecisionToggle />
445+
{/* Density toggle — persisted across sessions */}
446+
<FeedDensityToggle />
447+
</div>
426448
{/* #574: last-updated / stale status, with a manual refresh action */}
427449
<div className="flex items-center gap-2">
428450
<SyncStatusIndicator status={syncStatus} />
@@ -435,11 +457,6 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
435457
>
436458
Refresh
437459
</button>
438-
<div className="flex items-center gap-2">
439-
{/* Price precision toggle */}
440-
<PricePrecisionToggle />
441-
{/* Density toggle — persisted across sessions */}
442-
<FeedDensityToggle />
443460
</div>
444461
{/* #98: show consistent loading state */}
445462
<div
@@ -545,41 +562,6 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
545562
availableMarkets={availableAssets}
546563
/>
547564

548-
<div
549-
ref={parentRef}
550-
className="max-h-[70vh] overflow-auto"
551-
role="feed"
552-
aria-busy={isLoading}
553-
aria-label="Signal list"
554-
onKeyDown={(e) => {
555-
if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
556-
const articles = Array.from(
557-
(e.currentTarget as HTMLElement).querySelectorAll<HTMLElement>("article[tabindex]")
558-
);
559-
const idx = articles.indexOf(document.activeElement as HTMLElement);
560-
if (idx === -1) return;
561-
e.preventDefault();
562-
const next = e.key === "ArrowDown" ? articles[idx + 1] : articles[idx - 1];
563-
next?.focus();
564-
}}
565-
>
566-
{isError && (
567-
<DataPanelError
568-
errorInfo={classifyError(error)}
569-
onRetry={() => refetch()}
570-
/>
571-
)}
572-
573-
{isError && signals.length > 0 && (
574-
<div className="mb-3">
575-
<NetworkErrorState
576-
context="the latest signals"
577-
onRetry={() => refetch()}
578-
variant="banner"
579-
/>
580-
</div>
581-
)}
582-
583565
<div
584566
ref={splitContainerRef}
585567
className="lg:grid lg:items-start lg:gap-0"
@@ -595,7 +577,10 @@ export function SignalFeed({ initialData }: SignalFeedProps = {}) {
595577
>
596578
<div className="min-w-0">
597579
<div
598-
ref={setScrollEl}
580+
ref={(el) => {
581+
setScrollEl(el);
582+
parentRef.current = el;
583+
}}
599584
className="max-h-[70vh] overflow-auto"
600585
role="feed"
601586
aria-busy={isLoading}

0 commit comments

Comments
 (0)