From d9cf499dba6764db686f98d9c320c966493b7edc Mon Sep 17 00:00:00 2001 From: null Date: Sun, 7 Jun 2026 01:28:35 -0500 Subject: [PATCH] feat: search filter panel component, search preference persistence, page integration --- client/components/SearchFilterPanel.jsx | 64 +++++++++++++ client/hooks/useSearchPanelPreference.js | 32 +++++++ client/pages/BillsPage.jsx | 29 +++--- client/pages/SubscriptionsPage.jsx | 113 +++++++++++++++-------- client/pages/TrackerPage.jsx | 29 +++--- services/userSettings.js | 7 +- 6 files changed, 205 insertions(+), 69 deletions(-) create mode 100644 client/components/SearchFilterPanel.jsx create mode 100644 client/hooks/useSearchPanelPreference.js diff --git a/client/components/SearchFilterPanel.jsx b/client/components/SearchFilterPanel.jsx new file mode 100644 index 0000000..ab2f6d2 --- /dev/null +++ b/client/components/SearchFilterPanel.jsx @@ -0,0 +1,64 @@ +import { ChevronDown, ChevronUp, Search, X } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; + +export default function SearchFilterPanel({ + title = 'Search & filters', + collapsed, + onCollapsedChange, + hasFilters, + resultLabel, + sortLabel, + onClear, + children, + className, + variant = 'default', +}) { + const embedded = variant === 'embedded'; + const ToggleIcon = collapsed ? ChevronUp : ChevronDown; + + return ( +
+
+ + + {hasFilters && onClear && ( + + )} +
+ + {!collapsed && ( +
+ {children} +
+ )} +
+ ); +} diff --git a/client/hooks/useSearchPanelPreference.js b/client/hooks/useSearchPanelPreference.js new file mode 100644 index 0000000..6438841 --- /dev/null +++ b/client/hooks/useSearchPanelPreference.js @@ -0,0 +1,32 @@ +import { useCallback, useEffect, useState } from 'react'; +import { api } from '@/api'; + +const SETTING_KEY = 'search_bars_collapsed'; + +function settingToBool(value) { + return value === true || value === 'true' || value === '1' || value === 1; +} + +export function useSearchPanelPreference() { + const [collapsed, setCollapsed] = useState(false); + + useEffect(() => { + let mounted = true; + api.settings() + .then(settings => { + if (mounted && settings?.[SETTING_KEY] !== undefined && settings?.[SETTING_KEY] !== null) { + setCollapsed(settingToBool(settings[SETTING_KEY])); + } + }) + .catch(() => {}); + + return () => { mounted = false; }; + }, []); + + const saveCollapsed = useCallback((nextCollapsed) => { + setCollapsed(nextCollapsed); + api.saveSettings({ [SETTING_KEY]: nextCollapsed ? 'true' : 'false' }).catch(() => {}); + }, []); + + return [collapsed, saveCollapsed]; +} diff --git a/client/pages/BillsPage.jsx b/client/pages/BillsPage.jsx index a4edbec..87b8e1b 100644 --- a/client/pages/BillsPage.jsx +++ b/client/pages/BillsPage.jsx @@ -1,10 +1,11 @@ import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { Plus, ChevronRight, SlidersHorizontal, Search, Trash2, X } from 'lucide-react'; +import { Plus, ChevronRight, SlidersHorizontal, Search, Trash2 } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import SearchFilterPanel from '@/components/SearchFilterPanel'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, @@ -18,6 +19,7 @@ import { } from '@/components/ui/select'; import { api } from '@/api'; import { cn } from '@/lib/utils'; +import { useSearchPanelPreference } from '@/hooks/useSearchPanelPreference'; import { moveInArray, movedItemId, reorderPayload } from '@/lib/reorder'; import BillsTableInner from '@/components/BillsTableInner'; import BillModal from '@/components/BillModal'; @@ -629,6 +631,7 @@ export default function BillsPage() { const [billsSort, setBillsSort] = useState(() => ( localStorage.getItem(BILLS_SORT_KEY) === 'cadence' ? 'cadence' : 'custom' )); + const [searchPanelCollapsed, setSearchPanelCollapsed] = useSearchPanelPreference(); useEffect(() => { localStorage.setItem(BILLS_SORT_KEY, billsSort); @@ -944,8 +947,16 @@ export default function BillsPage() { -
-
+ +
toggleFilter('autopay')}>Autopay @@ -998,7 +999,7 @@ export default function BillsPage() { {filteredBills.length} of {bills.length} shown
-
+ {/* ── Active Bills ── */} {!filters.inactive && ( diff --git a/client/pages/SubscriptionsPage.jsx b/client/pages/SubscriptionsPage.jsx index 8b67f29..caeb2e2 100644 --- a/client/pages/SubscriptionsPage.jsx +++ b/client/pages/SubscriptionsPage.jsx @@ -30,6 +30,7 @@ import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; +import SearchFilterPanel from '@/components/SearchFilterPanel'; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; @@ -45,6 +46,7 @@ import { import BillModal from '@/components/BillModal'; import BillHistoricalImportDialog from '@/components/BillHistoricalImportDialog'; import { getLinkImportPref } from '@/pages/SettingsPage'; +import { useSearchPanelPreference } from '@/hooks/useSearchPanelPreference'; import { moveInArray, movedItemId, reorderPayload } from '@/lib/reorder'; const TYPE_LABELS = { @@ -967,6 +969,7 @@ export default function SubscriptionsPage() { const [draggingId, setDraggingId] = useState(null); const [dropTargetId, setDropTargetId] = useState(null); const [movingBillId, setMovingBillId] = useState(null); + const [searchPanelCollapsed, setSearchPanelCollapsed] = useSearchPanelPreference(); const [txQuery, setTxQuery] = useState(''); const [txResults, setTxResults] = useState([]); @@ -1415,25 +1418,35 @@ export default function SubscriptionsPage() {
-
- - setSubSearch(e.target.value)} - placeholder="Search subscriptions…" - className="h-8 pl-8 pr-8 text-sm" - /> - {subSearch && ( - - )} -
+ setSubSearch('')} + variant="embedded" + > +
+ + setSubSearch(e.target.value)} + placeholder="Search subscriptions…" + className="h-8 pl-8 pr-8 text-sm" + /> + {subSearch && ( + + )} +
+
{loading ? ( @@ -1470,16 +1483,26 @@ export default function SubscriptionsPage() { Known subscription services found in your bank transactions with 90%+ confidence. {!recommendationsLoading && highConfidenceRecs.length > 0 && ( -
- - setRecSearch(e.target.value)} - className="pl-8 h-8 text-sm" - /> -
+ setRecSearch('')} + variant="embedded" + > +
+ + setRecSearch(e.target.value)} + className="pl-8 h-8 text-sm" + /> +
+
)} @@ -1538,17 +1561,27 @@ export default function SubscriptionsPage() { Search all account charges — matched and unmatched — to find subscriptions the algorithm may have missed. -
- - setTxQuery(e.target.value)} - className="pl-8 h-9 text-sm" - autoComplete="off" - /> -
+ setTxQuery('')} + variant="embedded" + > +
+ + setTxQuery(e.target.value)} + className="pl-8 h-9 text-sm" + autoComplete="off" + /> +
+
{(txQuery.trim() || txSearching) && ( diff --git a/client/pages/TrackerPage.jsx b/client/pages/TrackerPage.jsx index f4100a0..62c2890 100644 --- a/client/pages/TrackerPage.jsx +++ b/client/pages/TrackerPage.jsx @@ -1,15 +1,17 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { ChevronLeft, ChevronRight, AlertCircle, CheckCircle2, Plus, Search, X, RefreshCw, Landmark, ArrowUpToLine, ArrowUp, ArrowDown } from 'lucide-react'; +import { ChevronLeft, ChevronRight, AlertCircle, CheckCircle2, Plus, Search, RefreshCw, Landmark, ArrowUpToLine, ArrowUp, ArrowDown } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api.js'; import { useTracker, useDriftReport } from '@/hooks/useQueries'; +import { useSearchPanelPreference } from '@/hooks/useSearchPanelPreference'; import BillModal from '@/components/BillModal'; import { makeBillDraft } from '@/lib/billDrafts'; import { scheduleLabel, scheduleValue } from '@/lib/billingSchedule'; import { cn, fmt } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import SearchFilterPanel from '@/components/SearchFilterPanel'; import { Skeleton } from '@/components/ui/Skeleton'; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, @@ -124,6 +126,7 @@ export default function TrackerPage() { const [incomeModalOpen, setIncomeModalOpen] = useState(false); const [orderedRows, setOrderedRows] = useState(null); const [movingBillId, setMovingBillId] = useState(null); + const [searchPanelCollapsed, setSearchPanelCollapsed] = useSearchPanelPreference(); // Row to open in PaymentLedgerDialog via the overdue command center const [commandCenterPayRow, setCommandCenterPayRow] = useState(null); @@ -496,8 +499,16 @@ export default function TrackerPage() { )} -
-
+ +
toggleFilter('unpaid')}>Unpaid @@ -577,7 +578,7 @@ export default function TrackerPage() { )}
-
+ {/* ── Summary cards (backend already excludes skipped from totals) ── */} {loading ? ( diff --git a/services/userSettings.js b/services/userSettings.js index 56b4128..c0a1c94 100644 --- a/services/userSettings.js +++ b/services/userSettings.js @@ -12,12 +12,17 @@ const USER_SETTING_KEYS = [ 'bank_tracking_account_id', 'bank_tracking_pending_days', 'bank_late_attribution_days', + 'search_bars_collapsed', ]; +const USER_SETTING_DEFAULTS = { + search_bars_collapsed: 'false', +}; + function defaultUserSettings() { const defaults = {}; for (const key of USER_SETTING_KEYS) { - defaults[key] = getSetting(key); + defaults[key] = USER_SETTING_DEFAULTS[key] ?? getSetting(key); } return defaults; }