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) <noreply@anthropic.com>
This commit is contained in:
parent
021198ab0f
commit
0238716acf
5 changed files with 81 additions and 21 deletions
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -17,14 +17,12 @@ export function useActivities() {
|
|||
const subscriptionError = ref<string | null>(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,
|
||||
|
|
|
|||
|
|
@ -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<TemporalFilter>(DEFAULT_FILTERS.temporal)
|
||||
const selectedCategories = ref<ActivityCategory[]>([])
|
||||
const searchQuery = ref('')
|
||||
const selectedDate = ref<Date | undefined>(undefined)
|
||||
|
||||
const filters = computed<ActivityFilters>(() => ({
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ const {
|
|||
selectedCategories,
|
||||
searchQuery,
|
||||
hasActiveFilters,
|
||||
selectedDate,
|
||||
selectDate,
|
||||
setTemporal,
|
||||
toggleCategory,
|
||||
clearCategories,
|
||||
resetFilters,
|
||||
|
|
@ -89,12 +92,12 @@ function handleRefresh() {
|
|||
|
||||
<!-- Date picker strip (p'a semana style) -->
|
||||
<div class="mb-4">
|
||||
<DatePickerStrip />
|
||||
<DatePickerStrip :selected-date="selectedDate" @select="selectDate" />
|
||||
</div>
|
||||
|
||||
<!-- Temporal filter pills -->
|
||||
<div class="mb-4">
|
||||
<TemporalFilterBar v-model="temporal" />
|
||||
<TemporalFilterBar :model-value="temporal" @update:model-value="setTemporal" />
|
||||
</div>
|
||||
|
||||
<!-- Category filters (collapsible) -->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue