1
Fork 0
mirror of https://github.com/thegeneralist01/archivr synced 2026-07-21 18:55:36 +02:00

feat(frontend): persist search query and tag filter in URL params

Extend parseLocation() to read ?q= and ?tag= search parameters.
Initialise searchQuery and tagFilter from the URL on mount so that
sharing or refreshing a filtered view restores the exact same
results.

- replaceState on every q/tag change (no extra history entries)
- pushState on view navigation now preserves existing search params
- popstate handler restores q and tag on back/forward
- firstArchiveLoad ref prevents the archive-change effect from
  wiping URL-initialised filters on the first mount
This commit is contained in:
TheGeneralist 2026-07-13 14:28:30 +02:00
parent 4ca1dd7872
commit bb4e57e1a2
Signed by: thegeneralist01
SSH key fingerprint: SHA256:pp9qddbCNmVNoSjevdvQvM5z0DHN7LTa8qBMbcMq/R4

View file

@ -33,7 +33,10 @@ function parseLocation() {
const parts = window.location.pathname.split('/').filter(Boolean)
const view = VIEWS.includes(parts[0]) ? parts[0] : 'archive'
const settingsTab = (view === 'settings' && SETTINGS_TABS.includes(parts[1])) ? parts[1] : 'profile'
return { view, settingsTab }
const params = new URLSearchParams(window.location.search)
const q = params.get('q') ?? ''
const tag = params.get('tag') ?? null
return { view, settingsTab, q, tag }
}
function locationPath(view, settingsTab) {
@ -66,9 +69,12 @@ export default function App() {
// Sync URL state on back/forward
useEffect(() => {
const handler = () => {
const { view, settingsTab } = parseLocation()
const { view, settingsTab, q, tag } = parseLocation()
setView(view)
setSettingsTab(settingsTab)
setSearchQuery(q)
setTagFilter(tag)
setSelectedEntry(null)
}
window.addEventListener('popstate', handler)
return () => window.removeEventListener('popstate', handler)
@ -79,10 +85,10 @@ export default function App() {
const [entries, setEntries] = useState([])
const [selectedEntryUid, setSelectedEntryUid] = useState(null)
const [selectedEntry, setSelectedEntry] = useState(null)
const [tagFilter, setTagFilter] = useState(null)
const [tagFilter, setTagFilter] = useState(() => parseLocation().tag)
const [view, setView] = useState(() => parseLocation().view)
const [settingsTab, setSettingsTab] = useState(() => parseLocation().settingsTab)
const [searchQuery, setSearchQuery] = useState('')
const [searchQuery, setSearchQuery] = useState(() => parseLocation().q)
const [resultCount, setResultCount] = useState('')
const [searchBusy, setSearchBusy] = useState(false)
const [runs, setRuns] = useState([])
@ -104,6 +110,7 @@ export default function App() {
const detailSeqRef = useRef(0)
const searchInputRef = useRef(null)
const pendingSearchFocus = useRef(false)
const firstArchiveLoad = useRef(true)
const humanizeTags = currentUser?.humanize_slugs ?? false;
@ -159,6 +166,16 @@ export default function App() {
// Archive change: parallel load entries + runs + tags
useEffect(() => {
if (!archiveId) return
if (firstArchiveLoad.current) {
// First load: URL-initialized filters are already in state; the debounced
// search and tagFilter effects will call loadEntries with the right values.
firstArchiveLoad.current = false
Promise.all([
fetchRuns(archiveId).then(setRuns),
fetchTags(archiveId).then(setTagNodes),
])
return
}
setTagFilter(null)
setSelectedEntry(null)
setSelectedEntryUid(null)
@ -198,13 +215,13 @@ export default function App() {
}
}, [archiveId])
// Sync view + settingsTab URL
// Sync view + settingsTab URL (skip when serving a standalone preview page)
// Preserve existing search params so ?q/tag/entry survive view navigation.
useEffect(() => {
if (PREVIEW_ROUTE) return
const path = locationPath(view, settingsTab)
if (window.location.pathname !== path) {
history.pushState(null, '', path)
history.pushState(null, '', path + window.location.search)
}
}, [view, settingsTab])
@ -268,6 +285,18 @@ export default function App() {
setSelectedEntryUid(prev => prev === entryUid ? null : prev)
}, [])
// Sync search params URL via replaceState (no new history entry).
useEffect(() => {
if (PREVIEW_ROUTE) return
const params = new URLSearchParams()
if (searchQuery) params.set('q', searchQuery)
if (tagFilter) params.set('tag', tagFilter)
const qs = params.toString()
const url = window.location.pathname + (qs ? '?' + qs : '')
const current = window.location.pathname + window.location.search
if (current !== url) history.replaceState(null, '', url)
}, [searchQuery, tagFilter])
// K / Ctrl+K: focus the search input, switching to archive view first if needed.
useEffect(() => {
const handler = (e) => {
@ -296,6 +325,7 @@ export default function App() {
})
}
}, [view])
const handleCaptureClick = useCallback(() => {
setCaptureDialogOpen(true)
}, [])