From 0238716acf474ba438605686915389c5adbb8b52 Mon Sep 17 00:00:00 2001 From: Padreug Date: Mon, 20 Apr 2026 08:09:37 +0200 Subject: [PATCH] Fix date filtering and timestamp parsing - parseTimestamp now handles unix seconds, milliseconds, and ISO strings; returns NaN for unparseable values instead of 0 (which caused Jan 1 1970) - Events with unparseable start timestamps are rejected by the parser - ActivityCard safely handles invalid dates without crashing - DatePickerStrip day selection now filters activities to that date - Selecting a day clears temporal pills; selecting a pill clears the day - Feed shows all activities sorted by date (no more upcoming/past split) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../activities/components/ActivityCard.vue | 15 +++++--- .../activities/composables/useActivities.ts | 13 +++---- .../composables/useActivityFilters.ts | 33 +++++++++++++++--- src/modules/activities/types/nip52.ts | 34 +++++++++++++++++-- .../activities/views/ActivitiesPage.vue | 7 ++-- 5 files changed, 81 insertions(+), 21 deletions(-) diff --git a/src/modules/activities/components/ActivityCard.vue b/src/modules/activities/components/ActivityCard.vue index 6503536..5eb1184 100644 --- a/src/modules/activities/components/ActivityCard.vue +++ b/src/modules/activities/components/ActivityCard.vue @@ -19,12 +19,17 @@ const { t } = useI18n() const dateDisplay = computed(() => { const a = props.activity - if (a.type === 'date') { - return format(a.startDate, 'EEE, MMM d') + if (!a.startDate || isNaN(a.startDate.getTime())) return '' + try { + if (a.type === 'date') { + return format(a.startDate, 'EEE, MMM d') + } + const date = format(a.startDate, 'EEE, MMM d') + const time = format(a.startDate, 'HH:mm') + return `${date} \u2022 ${time}` + } catch { + return '' } - const date = format(a.startDate, 'EEE, MMM d') - const time = format(a.startDate, 'HH:mm') - return `${date} \u2022 ${time}` }) const categoryLabel = computed(() => { diff --git a/src/modules/activities/composables/useActivities.ts b/src/modules/activities/composables/useActivities.ts index 7d4186b..edc4b02 100644 --- a/src/modules/activities/composables/useActivities.ts +++ b/src/modules/activities/composables/useActivities.ts @@ -17,14 +17,12 @@ export function useActivities() { const subscriptionError = ref(null) let unsubscribe: (() => void) | null = null - // Filtered and sorted activities + // Filtered and sorted activities (from all activities, filters handle time range) const filteredActivities = computed(() => { - const upcoming = store.upcomingActivities - return filters.applyFilters(upcoming) - }) - - const pastFilteredActivities = computed(() => { - return filters.applyFilters(store.pastActivities) + const all = store.activities.sort( + (a, b) => a.startDate.getTime() - b.startDate.getTime() + ) + return filters.applyFilters(all) }) /** @@ -113,7 +111,6 @@ export function useActivities() { return { // State activities: filteredActivities, - pastActivities: pastFilteredActivities, allActivities: computed(() => store.activities), isLoading: computed(() => store.isLoading), isSubscribed, diff --git a/src/modules/activities/composables/useActivityFilters.ts b/src/modules/activities/composables/useActivityFilters.ts index a42016c..0fb3742 100644 --- a/src/modules/activities/composables/useActivityFilters.ts +++ b/src/modules/activities/composables/useActivityFilters.ts @@ -1,7 +1,7 @@ import { ref, computed } from 'vue' import { startOfDay, endOfDay, startOfWeek, endOfWeek, - startOfMonth, endOfMonth, addDays, + startOfMonth, endOfMonth, addDays, isSameDay, } from 'date-fns' import type { Activity } from '../types/activity' import type { ActivityCategory } from '../types/category' @@ -15,6 +15,7 @@ export function useActivityFilters() { const temporal = ref(DEFAULT_FILTERS.temporal) const selectedCategories = ref([]) const searchQuery = ref('') + const selectedDate = ref(undefined) const filters = computed(() => ({ temporal: temporal.value, @@ -28,8 +29,18 @@ export function useActivityFilters() { function applyFilters(activities: Activity[]): Activity[] { let result = activities - // Temporal filter - result = applyTemporalFilter(result, temporal.value) + // Specific date filter (from DatePickerStrip) takes priority over temporal + if (selectedDate.value) { + const dayStart = startOfDay(selectedDate.value) + const dayEnd = endOfDay(selectedDate.value) + result = result.filter(a => { + const activityEnd = a.endDate ?? a.startDate + return a.startDate <= dayEnd && activityEnd >= dayStart + }) + } else { + // Temporal filter + result = applyTemporalFilter(result, temporal.value) + } // Category filter if (selectedCategories.value.length > 0) { @@ -54,6 +65,16 @@ export function useActivityFilters() { function setTemporal(value: TemporalFilter) { temporal.value = value + selectedDate.value = undefined // clear date pick when using temporal pills + } + + function selectDate(date: Date) { + if (selectedDate.value && isSameDay(selectedDate.value, date)) { + selectedDate.value = undefined // toggle off + } else { + selectedDate.value = date + temporal.value = 'all' // clear temporal pill when picking a specific date + } } function toggleCategory(category: ActivityCategory) { @@ -73,12 +94,14 @@ export function useActivityFilters() { temporal.value = DEFAULT_FILTERS.temporal selectedCategories.value = [] searchQuery.value = '' + selectedDate.value = undefined } const hasActiveFilters = computed(() => temporal.value !== 'all' || selectedCategories.value.length > 0 || - searchQuery.value.trim().length > 0 + searchQuery.value.trim().length > 0 || + selectedDate.value !== undefined ) return { @@ -86,12 +109,14 @@ export function useActivityFilters() { temporal, selectedCategories, searchQuery, + selectedDate, filters, hasActiveFilters, // Actions applyFilters, setTemporal, + selectDate, toggleCategory, clearCategories, resetFilters, diff --git a/src/modules/activities/types/nip52.ts b/src/modules/activities/types/nip52.ts index a83cc51..2a57a71 100644 --- a/src/modules/activities/types/nip52.ts +++ b/src/modules/activities/types/nip52.ts @@ -96,6 +96,32 @@ function getTagValues(tags: string[][], tagName: string): string[] { return tags.filter(t => t[0] === tagName).map(t => t[1]) } +/** + * Parse a NIP-52 start/end tag value to a unix timestamp in seconds. + * Handles: unix seconds, unix milliseconds, and ISO date strings. + */ +/** + * Parse a NIP-52 start/end tag value to a unix timestamp in seconds. + * Handles: unix seconds, unix milliseconds, and ISO date strings. + * Returns NaN if unparseable (caller must handle). + */ +function parseTimestamp(value: string): number { + const num = Number(value) + if (!isNaN(num) && num > 0) { + // If the number is unreasonably large, it's likely milliseconds + if (num > 1e12) { + return Math.floor(num / 1000) + } + return num + } + // Try as ISO date string + const date = new Date(value) + if (!isNaN(date.getTime())) { + return Math.floor(date.getTime() / 1000) + } + return NaN +} + /** * Parse a Nostr event into a CalendarTimeEvent (kind 31923) */ @@ -108,7 +134,11 @@ export function parseCalendarTimeEvent(event: NostrEvent): CalendarTimeEvent | n if (!dTag || !title || !startStr) return null + const startTs = parseTimestamp(startStr) + if (isNaN(startTs)) return null // reject events with unparseable timestamps + const endStr = getTagValue(event.tags, 'end') + const endTs = endStr ? parseTimestamp(endStr) : undefined const participants: Participant[] = event.tags .filter(t => t[0] === 'p') @@ -125,8 +155,8 @@ export function parseCalendarTimeEvent(event: NostrEvent): CalendarTimeEvent | n summary: getTagValue(event.tags, 'summary'), content: event.content, image: getTagValue(event.tags, 'image'), - start: parseInt(startStr, 10), - end: endStr ? parseInt(endStr, 10) : undefined, + start: startTs, + end: isNaN(endTs as number) ? undefined : endTs, startTzid: getTagValue(event.tags, 'start_tzid'), endTzid: getTagValue(event.tags, 'end_tzid'), location: getTagValue(event.tags, 'location'), diff --git a/src/modules/activities/views/ActivitiesPage.vue b/src/modules/activities/views/ActivitiesPage.vue index 6902b58..32857ba 100644 --- a/src/modules/activities/views/ActivitiesPage.vue +++ b/src/modules/activities/views/ActivitiesPage.vue @@ -33,6 +33,9 @@ const { selectedCategories, searchQuery, hasActiveFilters, + selectedDate, + selectDate, + setTemporal, toggleCategory, clearCategories, resetFilters, @@ -89,12 +92,12 @@ function handleRefresh() {
- +
- +