diff --git a/client/src/lib/components/SearchBar.svelte b/client/src/lib/components/SearchBar.svelte index d7837994..cd7d5253 100644 --- a/client/src/lib/components/SearchBar.svelte +++ b/client/src/lib/components/SearchBar.svelte @@ -8,6 +8,19 @@ const sortValue = $derived(`${appState.sortBy}_${appState.sortOrder}`); + const dateFrom = $derived(appState.timestampFrom != undefined ? new Date(appState.timestampFrom * 1000).toISOString().slice(0, 10) : ''); + const dateTo = $derived(appState.timestampTo != undefined ? new Date(appState.timestampTo * 1000).toISOString().slice(0, 10) : ''); + + function handleDateFromChange(event: Event) { + const value = (event.target as HTMLInputElement).value; + appState.timestampFrom = value ? Math.floor(new Date(value).getTime() / 1000) : undefined; + } + + function handleDateToChange(event: Event) { + const value = (event.target as HTMLInputElement).value; + appState.timestampTo = value ? Math.floor(new Date(value).getTime() / 1000) : undefined; + } + function handleSortChange(event: Event) { const target = event.target as HTMLSelectElement; if (target) { @@ -52,6 +65,12 @@ +
+ + + + +
{#if appState.viewMode === 'grid'} @@ -97,4 +116,13 @@ .search-input-right { @apply absolute right-0 inset-y-0 flex items-center px-4 leading-none text-gray-600 hover:text-gray-800 dark:text-gray-300 dark:hover:text-white cursor-pointer transition-opacity; } + + .date-input { + @apply h-10 rounded-md border border-gray-300 bg-gray-50 px-3 text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:focus:ring-blue-500; + color-scheme: light dark; + } + + .date-label { + @apply text-sm text-gray-700 dark:text-gray-300; + } diff --git a/client/src/lib/store.svelte.ts b/client/src/lib/store.svelte.ts index bfc1d181..f4867225 100644 --- a/client/src/lib/store.svelte.ts +++ b/client/src/lib/store.svelte.ts @@ -7,6 +7,8 @@ function createAppState() { let query = $state(''); let everywhere = $state(false); let future = $state(true); + let timestampFrom = $state(undefined); + let timestampTo = $state(undefined); let sortBy = $state('timestamp'); let sortOrder = $state('desc'); let currentPage = $state(0); @@ -55,6 +57,8 @@ function createAppState() { sortBy, sortOrder, future, + timestamp_from: timestampFrom, + timestamp_to: timestampTo, duration_min: parsedQuery.duration_min, duration_max: parsedQuery.duration_max, offset: currentPage * itemsPerPage, @@ -107,6 +111,11 @@ function createAppState() { everywhere = params['everywhere'] === 'true'; future = params['future'] !== 'false'; + const from = parseInt(params['timestamp_from'], 10); + timestampFrom = !isNaN(from) ? from : undefined; + const to = parseInt(params['timestamp_to'], 10); + timestampTo = !isNaN(to) ? to : undefined; + const sBy = params['sortBy']; if (sBy && isSortBy(sBy)) sortBy = sBy; else sortBy = 'timestamp'; @@ -135,6 +144,8 @@ function createAppState() { query, everywhere, future, + timestamp_from: timestampFrom, + timestamp_to: timestampTo, sortBy, sortOrder, page: currentPage + 1 @@ -171,12 +182,14 @@ function createAppState() { const cleanup = $effect.root(() => { $effect(() => { _search(); - trackSearch({ query, everywhere, future, sortBy, sortOrder, itemsPerPage, page: currentPage + 1 }); + trackSearch({ query, everywhere, future, timestampFrom, timestampTo, sortBy, sortOrder, itemsPerPage, page: currentPage + 1 }); updateUrlHash({ query: query || undefined, everywhere: everywhere || undefined, future: future ? undefined : 'false', + timestamp_from: timestampFrom, + timestamp_to: timestampTo, sortBy: sortBy === 'timestamp' ? undefined : sortBy, sortOrder: sortOrder === 'desc' ? undefined : sortOrder, page: currentPage > 0 ? currentPage + 1 : undefined @@ -219,6 +232,10 @@ function createAppState() { set everywhere(value) { everywhere = value; currentPage = 0; }, get future() { return future; }, set future(value) { future = value; currentPage = 0; }, + get timestampFrom() { return timestampFrom; }, + set timestampFrom(value) { timestampFrom = value; currentPage = 0; }, + get timestampTo() { return timestampTo; }, + set timestampTo(value) { timestampTo = value; currentPage = 0; }, get sortBy() { return sortBy; }, get sortOrder() { return sortOrder; }, get currentPage() { return currentPage; }, diff --git a/server/RSSFeedGenerator.ts b/server/RSSFeedGenerator.ts index 10e866cf..fd182f81 100644 --- a/server/RSSFeedGenerator.ts +++ b/server/RSSFeedGenerator.ts @@ -71,6 +71,8 @@ export class RSSFeedGenerator { sortBy: urlQuery.sortBy ?? 'timestamp', sortOrder: urlQuery.sortOrder ?? 'desc', future, + timestamp_from: (typeof urlQuery.timestamp_from == 'string') ? parseInt(urlQuery.timestamp_from) : undefined, + timestamp_to: (typeof urlQuery.timestamp_to == 'string') ? parseInt(urlQuery.timestamp_to) : undefined, duration_min: parsedQuery.duration_min, duration_max: parsedQuery.duration_max, offset: (typeof urlQuery.offset == 'string') ? parseInt(urlQuery.offset) : 0, diff --git a/server/SearchEngine.ts b/server/SearchEngine.ts index 9799326c..eba41367 100644 --- a/server/SearchEngine.ts +++ b/server/SearchEngine.ts @@ -148,7 +148,23 @@ export class SearchEngine { opensearchQuery.body.query.bool.filter.push(durationFilter); } - if (query.future === false) { + const hasExplicitTimestampRange = query.timestamp_from != undefined || query.timestamp_to != undefined; + if (hasExplicitTimestampRange) { + const timestampFilter: { range: { timestamp: { gte?: number, lte?: number } } } = { + range: { timestamp: {} } + }; + + if (query.timestamp_from != undefined) { + timestampFilter.range.timestamp.gte = query.timestamp_from; + } + + if (query.timestamp_to != undefined) { + timestampFilter.range.timestamp.lte = query.timestamp_to; + } + opensearchQuery.body.query.bool.filter.push(timestampFilter); + } + + if (query.future === false && !hasExplicitTimestampRange) { opensearchQuery.body.query.bool.filter.push({ range: { timestamp: { to: 'now+1h/h' }