diff --git a/frontend/src/components/FilterBar.vue b/frontend/src/components/FilterBar.vue index 002a1d2..e04406a 100644 --- a/frontend/src/components/FilterBar.vue +++ b/frontend/src/components/FilterBar.vue @@ -6,6 +6,7 @@ import { useSavedFiltersStore } from "../stores/savedFilters"; import { useUiStore } from "../stores/ui"; import type { NoteFacets } from "../stores/notes"; import { facetCount, facetsFromQuery, facetsToQuery } from "../notes/facets"; +import { addLocalDays, formatLocalDay, parseLocalDate } from "../notes/datetime"; import { NOTE_COLOR_KEYS, NOTE_COLOR_LABELS, NOTE_SWATCH_CLASSES, type NoteColor } from "../notes/colors"; import Icon from "./Icon.vue"; @@ -56,30 +57,25 @@ function onQ(e: Event) { qTimer = setTimeout(() => patch({ q: v.trim() || undefined }), 300); } -function isoDay(d: Date): string { - return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; -} function onFrom(e: Event) { const v = (e.target as HTMLInputElement).value; patch({ created_after: v ? `${v}T00:00:00` : undefined }); } function onTo(e: Event) { const v = (e.target as HTMLInputElement).value; - if (!v) { + const d = v ? parseLocalDate(v) : null; + if (!d) { patch({ created_before: undefined }); return; } // Half-open upper bound: the start of the day AFTER the chosen date (so it's inclusive). - const d = new Date(`${v}T00:00:00`); - d.setDate(d.getDate() + 1); - patch({ created_before: `${isoDay(d)}T00:00:00` }); + patch({ created_before: `${formatLocalDay(addLocalDays(d, 1))}T00:00:00` }); } const fromInput = computed(() => (facets.value.created_after ?? "").slice(0, 10)); const toInput = computed(() => { - if (!facets.value.created_before) return ""; - const d = new Date(facets.value.created_before); - d.setDate(d.getDate() - 1); - return isoDay(d); + // Reverse the half-open bound (day-after) back to the chosen end day for the input. + const d = facets.value.created_before ? parseLocalDate(facets.value.created_before.slice(0, 10)) : null; + return d ? formatLocalDay(addLocalDays(d, -1)) : ""; }); async function saveView() { diff --git a/frontend/src/notes/datetime.ts b/frontend/src/notes/datetime.ts index 8da1cfe..c821733 100644 --- a/frontend/src/notes/datetime.ts +++ b/frontend/src/notes/datetime.ts @@ -26,3 +26,26 @@ export function formatReminder(iso: string | null): string { export function isOverdue(iso: string | null): boolean { return !!iso && new Date(iso).getTime() < Date.now(); } + +// --- Local calendar-date helpers (the created-date range facets work in the viewer's +// LOCAL day; the board's FilterBar and the Timeline view both build day bounds). --- + +// Parse an value ("YYYY-MM-DD") as a LOCAL calendar date (midnight +// local), or null if it isn't a valid YYYY-MM-DD. +export function parseLocalDate(value: string): Date | null { + const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); + if (!m) return null; + return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])); +} + +// A new local Date shifted by whole days — e.g. +1 for the half-open upper bound that +// includes the whole end day, -1 to reverse it for display. Non-mutating. +export function addLocalDays(d: Date, days: number): Date { + return new Date(d.getFullYear(), d.getMonth(), d.getDate() + days); +} + +// Format a local Date as "YYYY-MM-DD" (the shape an reads). +export function formatLocalDay(d: Date): string { + const pad = (n: number) => String(n).padStart(2, "0"); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; +} diff --git a/frontend/src/views/TimelineView.vue b/frontend/src/views/TimelineView.vue index 6cb2c9a..8ddf86d 100644 --- a/frontend/src/views/TimelineView.vue +++ b/frontend/src/views/TimelineView.vue @@ -2,6 +2,7 @@ import { computed, onMounted, ref, watch } from "vue"; import { api } from "../api/client"; import { type Note } from "../stores/notes"; +import { addLocalDays, parseLocalDate } from "../notes/datetime"; import { useNoteList } from "../composables/useNoteList"; import { useNoteEditor } from "../composables/useNoteEditor"; import AsyncState from "../components/AsyncState.vue"; @@ -18,23 +19,13 @@ const fromDate = ref(""); const toDate = ref(""); const hasRange = computed(() => !!fromDate.value || !!toDate.value); -// Parse an value ("YYYY-MM-DD") as a LOCAL calendar date. -function localDate(value: string): Date | null { - const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value); - if (!m) return null; - return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])); -} - function buildQuery(): string { const params = new URLSearchParams({ filter: "active", sort: "created" }); - const from = localDate(fromDate.value); + const from = parseLocalDate(fromDate.value); if (from) params.set("created_after", from.toISOString()); - const to = localDate(toDate.value); - if (to) { - // Half-open upper bound: start of the day AFTER `to`, so the whole `to` day is included. - const end = new Date(to.getFullYear(), to.getMonth(), to.getDate() + 1); - params.set("created_before", end.toISOString()); - } + const to = parseLocalDate(toDate.value); + // Half-open upper bound: start of the day AFTER `to`, so the whole `to` day is included. + if (to) params.set("created_before", addLocalDays(to, 1).toISOString()); return params.toString(); }