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' }