From 9bd3799fbca5681aa07e24794e46fe8deb682465 Mon Sep 17 00:00:00 2001 From: cesnimda Date: Tue, 30 Jun 2026 22:00:12 +0200 Subject: [PATCH] feat: saved searches Lets users name and pin a search query from the SearchResults page; saved searches persist to localStorage and show as quick links in a new sidebar section, similar to Favorites. Clicking re-runs the query. Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/components/Layout.jsx | 36 ++++++++++++++++++++++++-- frontend/src/hooks/useSavedSearches.js | 31 ++++++++++++++++++++++ frontend/src/pages/SearchResults.jsx | 15 ++++++++++- 3 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 frontend/src/hooks/useSavedSearches.js diff --git a/frontend/src/components/Layout.jsx b/frontend/src/components/Layout.jsx index 112c6cc..8436985 100644 --- a/frontend/src/components/Layout.jsx +++ b/frontend/src/components/Layout.jsx @@ -4,6 +4,7 @@ import { AuthApi, SyncApi, AnalyticsApi } from '../api/client.js'; import Logo from './Logo.jsx'; import DevBanner from './DevBanner.jsx'; import SyncStatus from './SyncStatus.jsx'; +import useSavedSearches from '../hooks/useSavedSearches.js'; // ── Folder definitions ──────────────────────────────────────────────────────── @@ -78,8 +79,8 @@ const loadFavs = () => { const saveFavs = (v) => localStorage.setItem(LS_FAV, JSON.stringify(v)); const loadSections = () => { - try { return JSON.parse(localStorage.getItem(LS_SECTS)) ?? { fav: true, mailbox: true, smart: true }; } - catch { return { fav: true, mailbox: true, smart: true }; } + try { return JSON.parse(localStorage.getItem(LS_SECTS)) ?? { fav: true, mailbox: true, smart: true, saved: true }; } + catch { return { fav: true, mailbox: true, smart: true, saved: true }; } }; const saveSections = (v) => localStorage.setItem(LS_SECTS, JSON.stringify(v)); @@ -126,6 +127,7 @@ export default function Layout() { const [sections, setSections] = useState(loadSections); const [favSlugs, setFavSlugs] = useState(loadFavs); const [counts, setCounts] = useState(null); + const { searches: savedSearches, remove: removeSavedSearch } = useSavedSearches(); const loc = useLocation(); const navigate = useNavigate(); @@ -291,6 +293,36 @@ export default function Layout() { )} + {/* ── Saved Searches ── */} + {savedSearches.length > 0 && ( + <> + toggleSection('saved')} collapsed={!sidebarOpen} /> + {sections.saved && ( +
    + {savedSearches.map((s) => ( +
  • + + 🔎 + {sidebarOpen && {s.label}} + + {sidebarOpen && ( + + )} +
  • + ))} +
+ )} + + )} + {/* ── Mailbox ── */} toggleSection('mailbox')} collapsed={!sidebarOpen} /> {sections.mailbox && ( diff --git a/frontend/src/hooks/useSavedSearches.js b/frontend/src/hooks/useSavedSearches.js new file mode 100644 index 0000000..1d39f71 --- /dev/null +++ b/frontend/src/hooks/useSavedSearches.js @@ -0,0 +1,31 @@ +import { useCallback, useState } from 'react'; + +const LS_KEY = 'ii:saved-searches'; + +const load = () => { + try { return JSON.parse(localStorage.getItem(LS_KEY)) ?? []; } + catch { return []; } +}; +const save = (v) => localStorage.setItem(LS_KEY, JSON.stringify(v)); + +export default function useSavedSearches() { + const [searches, setSearches] = useState(load); + + const add = useCallback((label, query) => { + setSearches((prev) => { + const next = [...prev.filter((s) => s.query !== query), { id: crypto.randomUUID(), label, query }]; + save(next); + return next; + }); + }, []); + + const remove = useCallback((id) => { + setSearches((prev) => { + const next = prev.filter((s) => s.id !== id); + save(next); + return next; + }); + }, []); + + return { searches, add, remove }; +} diff --git a/frontend/src/pages/SearchResults.jsx b/frontend/src/pages/SearchResults.jsx index dd49eee..31115f7 100644 --- a/frontend/src/pages/SearchResults.jsx +++ b/frontend/src/pages/SearchResults.jsx @@ -5,6 +5,7 @@ import EmailRow from '../components/EmailRow.jsx'; import BulkToolbar from '../components/BulkToolbar.jsx'; import useSelection from '../hooks/useSelection.js'; import useListKeyboardNav from '../hooks/useListKeyboardNav.js'; +import useSavedSearches from '../hooks/useSavedSearches.js'; const PAGE_SIZE = 50; @@ -21,6 +22,13 @@ export default function SearchResults() { const [error, setError] = useState(null); const { selected, toggle, clear, removeIds, selectedIds } = useSelection(); const focusedId = useListKeyboardNav(emails, (id) => setEmails((prev) => prev.filter((x) => x.id !== id))); + const { searches: savedSearches, add: addSavedSearch } = useSavedSearches(); + const isSaved = savedSearches.some((s) => s.query === q); + + const handleSave = () => { + const label = window.prompt('Name this saved search:', q); + if (label && label.trim()) addSavedSearch(label.trim(), q); + }; useEffect(() => { setEmails([]); @@ -63,11 +71,16 @@ export default function SearchResults() { return (
-
+

🔍 "{q}"

{totalCount !== null && ( {totalCount.toLocaleString()} result{totalCount !== 1 ? 's' : ''} )} + {q.trim() && ( + + )}
{!q.trim() &&
Enter a search query above.
}