From b023d54264ebc25b5326aa5983c3dddd454d4151 Mon Sep 17 00:00:00 2001 From: padreug Date: Tue, 21 Oct 2025 23:14:56 +0200 Subject: [PATCH 01/10] Replaces custom expand/collapse with Collapsible Migrates ScheduledEventCard to use the Collapsible component from the UI library. This simplifies the component's structure and improves accessibility by leveraging the built-in features of the Collapsible component. Removes custom logic for managing the expanded/collapsed state. --- .../components/ScheduledEventCard.vue | 158 ++++++++---------- 1 file changed, 68 insertions(+), 90 deletions(-) diff --git a/src/modules/nostr-feed/components/ScheduledEventCard.vue b/src/modules/nostr-feed/components/ScheduledEventCard.vue index 5acfa51..b28fdef 100644 --- a/src/modules/nostr-feed/components/ScheduledEventCard.vue +++ b/src/modules/nostr-feed/components/ScheduledEventCard.vue @@ -10,6 +10,11 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog' +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/components/ui/collapsible' import { Calendar, MapPin, Clock, CheckCircle } from 'lucide-vue-next' import type { ScheduledEvent, EventCompletion } from '../services/ScheduledEventService' @@ -33,9 +38,6 @@ const emit = defineEmits() // Confirmation dialog state const showConfirmDialog = ref(false) -// Collapsed state (collapsed by default) -const isExpanded = ref(false) - // Event address for tracking completion const eventAddress = computed(() => `31922:${props.event.pubkey}:${props.event.dTag}`) @@ -116,81 +118,56 @@ function confirmMarkComplete() { function cancelMarkComplete() { showConfirmDialog.value = false } - -// Toggle expanded/collapsed state -function toggleExpanded() { - isExpanded.value = !isExpanded.value -} From 93e3e0a1cabcc567de861d6d02e034f3d9cc003e Mon Sep 17 00:00:00 2001 From: padreug Date: Tue, 21 Oct 2025 23:41:37 +0200 Subject: [PATCH 02/10] Filters scheduled events by participation Ensures users only see scheduled events they are participating in or events that are open to everyone. This change filters the list of today's scheduled events based on the current user's participation. It only displays events where the user is listed as a participant or events that do not have any participants specified. --- .../composables/useScheduledEvents.ts | 9 +++++-- .../services/ScheduledEventService.ts | 27 ++++++++++++++++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/modules/nostr-feed/composables/useScheduledEvents.ts b/src/modules/nostr-feed/composables/useScheduledEvents.ts index ae79702..42552c7 100644 --- a/src/modules/nostr-feed/composables/useScheduledEvents.ts +++ b/src/modules/nostr-feed/composables/useScheduledEvents.ts @@ -1,6 +1,7 @@ import { computed } from 'vue' import { injectService, SERVICE_TOKENS } from '@/core/di-container' import type { ScheduledEventService, ScheduledEvent, EventCompletion } from '../services/ScheduledEventService' +import type { AuthService } from '@/modules/base/auth/auth-service' import { useToast } from '@/core/composables/useToast' /** @@ -8,8 +9,12 @@ import { useToast } from '@/core/composables/useToast' */ export function useScheduledEvents() { const scheduledEventService = injectService(SERVICE_TOKENS.SCHEDULED_EVENT_SERVICE) + const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) const toast = useToast() + // Get current user's pubkey + const currentUserPubkey = computed(() => authService?.user.value?.pubkey) + /** * Get all scheduled events */ @@ -27,11 +32,11 @@ export function useScheduledEvents() { } /** - * Get today's scheduled events + * Get today's scheduled events (filtered by current user participation) */ const getTodaysEvents = (): ScheduledEvent[] => { if (!scheduledEventService) return [] - return scheduledEventService.getTodaysEvents() + return scheduledEventService.getTodaysEvents(currentUserPubkey.value) } /** diff --git a/src/modules/nostr-feed/services/ScheduledEventService.ts b/src/modules/nostr-feed/services/ScheduledEventService.ts index 19bda58..3b64c5a 100644 --- a/src/modules/nostr-feed/services/ScheduledEventService.ts +++ b/src/modules/nostr-feed/services/ScheduledEventService.ts @@ -16,6 +16,7 @@ export interface ScheduledEvent { location?: string status: string eventType?: string // 'task' for completable events, 'announcement' for informational + participants?: Array<{ pubkey: string; type?: string }> // 'required', 'optional', 'organizer' content: string tags: string[][] } @@ -79,6 +80,13 @@ export class ScheduledEventService extends BaseService { const status = event.tags.find(tag => tag[0] === 'status')?.[1] || 'pending' const eventType = event.tags.find(tag => tag[0] === 'event-type')?.[1] + // Parse participant tags: ["p", "", "", ""] + const participantTags = event.tags.filter(tag => tag[0] === 'p') + const participants = participantTags.map(tag => ({ + pubkey: tag[1], + type: tag[3] // 'required', 'optional', 'organizer' + })) + if (!start) { console.warn('Scheduled event missing start date:', event.id) return @@ -99,6 +107,7 @@ export class ScheduledEventService extends BaseService { location, status, eventType, + participants: participants.length > 0 ? participants : undefined, content: event.content, tags: event.tags } @@ -181,11 +190,23 @@ export class ScheduledEventService extends BaseService { } /** - * Get events for today + * Get events for today, optionally filtered by user participation */ - getTodaysEvents(): ScheduledEvent[] { + getTodaysEvents(userPubkey?: string): ScheduledEvent[] { const today = new Date().toISOString().split('T')[0] - return this.getEventsForDate(today) + const events = this.getEventsForDate(today) + + // If no user pubkey provided, return all events + if (!userPubkey) return events + + // Filter events based on participation + return events.filter(event => { + // If event has no participants, it's community-wide (show to everyone) + if (!event.participants || event.participants.length === 0) return true + + // Otherwise, only show if user is a participant + return event.participants.some(p => p.pubkey === userPubkey) + }) } /** From 62161dd0005abad9cd96c006defa799a9f57b701 Mon Sep 17 00:00:00 2001 From: padreug Date: Tue, 21 Oct 2025 23:41:46 +0200 Subject: [PATCH 03/10] Shows completer name on completed badge Updates the completed badge to display the name of the user who marked the event as complete. This provides better context and clarity regarding who triggered the completion status. --- src/modules/nostr-feed/components/ScheduledEventCard.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/nostr-feed/components/ScheduledEventCard.vue b/src/modules/nostr-feed/components/ScheduledEventCard.vue index b28fdef..f7954bd 100644 --- a/src/modules/nostr-feed/components/ScheduledEventCard.vue +++ b/src/modules/nostr-feed/components/ScheduledEventCard.vue @@ -151,9 +151,9 @@ function cancelMarkComplete() { - - - ✓ + + + ✓ {{ getDisplayName(getCompletion(eventAddress)!.pubkey) }} From 2cf737213bb41c5049273c30dc3a9885f9052ddf Mon Sep 17 00:00:00 2001 From: padreug Date: Wed, 22 Oct 2025 00:27:22 +0200 Subject: [PATCH 04/10] Filters and sorts scheduled events Improves scheduled event retrieval by filtering events based on user participation and sorting them by start time. This ensures that users only see events they are participating in or events that are open to the entire community. --- .../services/ScheduledEventService.ts | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/modules/nostr-feed/services/ScheduledEventService.ts b/src/modules/nostr-feed/services/ScheduledEventService.ts index 3b64c5a..6215e4b 100644 --- a/src/modules/nostr-feed/services/ScheduledEventService.ts +++ b/src/modules/nostr-feed/services/ScheduledEventService.ts @@ -194,19 +194,26 @@ export class ScheduledEventService extends BaseService { */ getTodaysEvents(userPubkey?: string): ScheduledEvent[] { const today = new Date().toISOString().split('T')[0] - const events = this.getEventsForDate(today) + let events = this.getEventsForDate(today) - // If no user pubkey provided, return all events - if (!userPubkey) return events + // Filter events based on participation (if user pubkey provided) + if (userPubkey) { + events = events.filter(event => { + // If event has no participants, it's community-wide (show to everyone) + if (!event.participants || event.participants.length === 0) return true - // Filter events based on participation - return events.filter(event => { - // If event has no participants, it's community-wide (show to everyone) - if (!event.participants || event.participants.length === 0) return true + // Otherwise, only show if user is a participant + return event.participants.some(p => p.pubkey === userPubkey) + }) + } - // Otherwise, only show if user is a participant - return event.participants.some(p => p.pubkey === userPubkey) + // Sort by start time (ascending order) + events.sort((a, b) => { + // ISO datetime strings can be compared lexicographically + return a.start.localeCompare(b.start) }) + + return events } /** From 0f0eae8800dfbd6e1bc27f73d03d3e5f36e157df Mon Sep 17 00:00:00 2001 From: padreug Date: Wed, 22 Oct 2025 00:16:47 +0200 Subject: [PATCH 05/10] Enables recurring scheduled event completion Extends scheduled event completion to support recurring events. The changes introduce the concept of an "occurrence" for recurring events, allowing users to mark individual instances of a recurring event as complete. This involves: - Adding recurrence information to the ScheduledEvent model. - Modifying completion logic to handle recurring events with daily/weekly frequencies - Updating UI to display recurrence information and mark individual occurrences as complete. --- .../nostr-feed/components/NostrFeed.vue | 6 +- .../components/ScheduledEventCard.vue | 36 +++-- .../composables/useScheduledEvents.ts | 12 +- .../services/ScheduledEventService.ts | 139 ++++++++++++++---- 4 files changed, 147 insertions(+), 46 deletions(-) diff --git a/src/modules/nostr-feed/components/NostrFeed.vue b/src/modules/nostr-feed/components/NostrFeed.vue index 1061ea5..7f22c2b 100644 --- a/src/modules/nostr-feed/components/NostrFeed.vue +++ b/src/modules/nostr-feed/components/NostrFeed.vue @@ -200,10 +200,10 @@ async function onToggleLike(note: FeedPost) { } // Handle scheduled event completion toggle -async function onToggleComplete(event: ScheduledEvent) { - console.log('🎯 NostrFeed: onToggleComplete called for event:', event.title) +async function onToggleComplete(event: ScheduledEvent, occurrence?: string) { + console.log('🎯 NostrFeed: onToggleComplete called for event:', event.title, 'occurrence:', occurrence) try { - await toggleComplete(event) + await toggleComplete(event, occurrence) console.log('✅ NostrFeed: toggleComplete succeeded') } catch (error) { console.error('❌ NostrFeed: Failed to toggle event completion:', error) diff --git a/src/modules/nostr-feed/components/ScheduledEventCard.vue b/src/modules/nostr-feed/components/ScheduledEventCard.vue index f7954bd..fbe50b9 100644 --- a/src/modules/nostr-feed/components/ScheduledEventCard.vue +++ b/src/modules/nostr-feed/components/ScheduledEventCard.vue @@ -21,12 +21,12 @@ import type { ScheduledEvent, EventCompletion } from '../services/ScheduledEvent interface Props { event: ScheduledEvent getDisplayName: (pubkey: string) => string - getCompletion: (eventAddress: string) => EventCompletion | undefined + getCompletion: (eventAddress: string, occurrence?: string) => EventCompletion | undefined adminPubkeys?: string[] } interface Emits { - (e: 'toggle-complete', event: ScheduledEvent): void + (e: 'toggle-complete', event: ScheduledEvent, occurrence?: string): void } const props = withDefaults(defineProps(), { @@ -41,11 +41,20 @@ const showConfirmDialog = ref(false) // Event address for tracking completion const eventAddress = computed(() => `31922:${props.event.pubkey}:${props.event.dTag}`) +// Check if this is a recurring event +const isRecurring = computed(() => !!props.event.recurrence) + +// For recurring events, occurrence is today's date. For non-recurring, it's undefined. +const occurrence = computed(() => { + if (!isRecurring.value) return undefined + return new Date().toISOString().split('T')[0] // YYYY-MM-DD +}) + // Check if this is an admin event const isAdminEvent = computed(() => props.adminPubkeys.includes(props.event.pubkey)) -// Check if event is completed - call function directly -const isCompleted = computed(() => props.getCompletion(eventAddress.value)?.completed || false) +// Check if event is completed - call function with occurrence for recurring events +const isCompleted = computed(() => props.getCompletion(eventAddress.value, occurrence.value)?.completed || false) // Check if event is completable (task type) const isCompletable = computed(() => props.event.eventType === 'task') @@ -109,8 +118,8 @@ function handleMarkComplete() { // Confirm and execute mark complete function confirmMarkComplete() { - console.log('✅ Confirmed mark complete for event:', props.event.title) - emit('toggle-complete', props.event) + console.log('✅ Confirmed mark complete for event:', props.event.title, 'occurrence:', occurrence.value) + emit('toggle-complete', props.event, occurrence.value) showConfirmDialog.value = false } @@ -152,8 +161,13 @@ function cancelMarkComplete() { - - ✓ {{ getDisplayName(getCompletion(eventAddress)!.pubkey) }} + + ✓ {{ getDisplayName(getCompletion(eventAddress, occurrence)!.pubkey) }} + + + + + 🔄 @@ -192,9 +206,9 @@ function cancelMarkComplete() { -
- ✓ Completed by {{ getDisplayName(getCompletion(eventAddress)!.pubkey) }} - - {{ getCompletion(eventAddress)!.notes }} +
+ ✓ Completed by {{ getDisplayName(getCompletion(eventAddress, occurrence)!.pubkey) }} + - {{ getCompletion(eventAddress, occurrence)!.notes }}
diff --git a/src/modules/nostr-feed/composables/useScheduledEvents.ts b/src/modules/nostr-feed/composables/useScheduledEvents.ts index 42552c7..2f24189 100644 --- a/src/modules/nostr-feed/composables/useScheduledEvents.ts +++ b/src/modules/nostr-feed/composables/useScheduledEvents.ts @@ -56,10 +56,10 @@ export function useScheduledEvents() { } /** - * Toggle completion status of an event + * Toggle completion status of an event (optionally for a specific occurrence) */ - const toggleComplete = async (event: ScheduledEvent, notes: string = ''): Promise => { - console.log('🔧 useScheduledEvents: toggleComplete called for event:', event.title) + const toggleComplete = async (event: ScheduledEvent, occurrence?: string, notes: string = ''): Promise => { + console.log('🔧 useScheduledEvents: toggleComplete called for event:', event.title, 'occurrence:', occurrence) if (!scheduledEventService) { console.error('❌ useScheduledEvents: Scheduled event service not available') @@ -69,16 +69,16 @@ export function useScheduledEvents() { try { const eventAddress = `31922:${event.pubkey}:${event.dTag}` - const currentlyCompleted = scheduledEventService.isCompleted(eventAddress) + const currentlyCompleted = scheduledEventService.isCompleted(eventAddress, occurrence) console.log('📊 useScheduledEvents: Current completion status:', currentlyCompleted) if (currentlyCompleted) { console.log('⬇️ useScheduledEvents: Marking as incomplete...') - await scheduledEventService.uncompleteEvent(event) + await scheduledEventService.uncompleteEvent(event, occurrence) toast.success('Event marked as incomplete') } else { console.log('⬆️ useScheduledEvents: Marking as complete...') - await scheduledEventService.completeEvent(event, notes) + await scheduledEventService.completeEvent(event, notes, occurrence) toast.success('Event completed!') } } catch (error) { diff --git a/src/modules/nostr-feed/services/ScheduledEventService.ts b/src/modules/nostr-feed/services/ScheduledEventService.ts index 6215e4b..17d9f43 100644 --- a/src/modules/nostr-feed/services/ScheduledEventService.ts +++ b/src/modules/nostr-feed/services/ScheduledEventService.ts @@ -4,6 +4,12 @@ import { injectService, SERVICE_TOKENS } from '@/core/di-container' import { finalizeEvent, type EventTemplate } from 'nostr-tools' import type { Event as NostrEvent } from 'nostr-tools' +export interface RecurrencePattern { + frequency: 'daily' | 'weekly' + dayOfWeek?: string // For weekly: 'monday', 'tuesday', etc. + endDate?: string // ISO date string - when to stop recurring (optional) +} + export interface ScheduledEvent { id: string pubkey: string @@ -19,11 +25,13 @@ export interface ScheduledEvent { participants?: Array<{ pubkey: string; type?: string }> // 'required', 'optional', 'organizer' content: string tags: string[][] + recurrence?: RecurrencePattern // Optional: for recurring events } export interface EventCompletion { id: string eventAddress: string // "31922:pubkey:d-tag" + occurrence?: string // ISO date string for the specific occurrence (YYYY-MM-DD) pubkey: string // Who completed it created_at: number completed: boolean @@ -87,6 +95,20 @@ export class ScheduledEventService extends BaseService { type: tag[3] // 'required', 'optional', 'organizer' })) + // Parse recurrence tags + const recurrenceFreq = event.tags.find(tag => tag[0] === 'recurrence')?.[1] as 'daily' | 'weekly' | undefined + const recurrenceDayOfWeek = event.tags.find(tag => tag[0] === 'recurrence-day')?.[1] + const recurrenceEndDate = event.tags.find(tag => tag[0] === 'recurrence-end')?.[1] + + let recurrence: RecurrencePattern | undefined + if (recurrenceFreq === 'daily' || recurrenceFreq === 'weekly') { + recurrence = { + frequency: recurrenceFreq, + dayOfWeek: recurrenceDayOfWeek, + endDate: recurrenceEndDate + } + } + if (!start) { console.warn('Scheduled event missing start date:', event.id) return @@ -109,7 +131,8 @@ export class ScheduledEventService extends BaseService { eventType, participants: participants.length > 0 ? participants : undefined, content: event.content, - tags: event.tags + tags: event.tags, + recurrence } // Store or update the event (replaceable by d-tag) @@ -138,9 +161,11 @@ export class ScheduledEventService extends BaseService { const completed = event.tags.find(tag => tag[0] === 'completed')?.[1] === 'true' const completedAtTag = event.tags.find(tag => tag[0] === 'completed_at')?.[1] const completedAt = completedAtTag ? parseInt(completedAtTag) : undefined + const occurrence = event.tags.find(tag => tag[0] === 'occurrence')?.[1] // ISO date string console.log('📋 Completion details:', { aTag, + occurrence, completed, pubkey: event.pubkey, eventId: event.id @@ -149,6 +174,7 @@ export class ScheduledEventService extends BaseService { const completion: EventCompletion = { id: event.id, eventAddress: aTag, + occurrence, pubkey: event.pubkey, created_at: event.created_at, completed, @@ -157,12 +183,15 @@ export class ScheduledEventService extends BaseService { } // Store completion (most recent one wins) - const existing = this._completions.get(aTag) + // For recurring events, include occurrence in the key: "eventAddress:occurrence" + // For non-recurring, just use eventAddress + const completionKey = occurrence ? `${aTag}:${occurrence}` : aTag + const existing = this._completions.get(completionKey) if (!existing || event.created_at > existing.created_at) { - this._completions.set(aTag, completion) - console.log('✅ Stored completion for:', aTag, '- completed:', completed) + this._completions.set(completionKey, completion) + console.log('✅ Stored completion for:', completionKey, '- completed:', completed) } else { - console.log('⏭️ Skipped older completion for:', aTag) + console.log('⏭️ Skipped older completion for:', completionKey) } } catch (error) { @@ -189,12 +218,55 @@ export class ScheduledEventService extends BaseService { }) } + /** + * Check if a recurring event occurs on a specific date + */ + private doesRecurringEventOccurOnDate(event: ScheduledEvent, targetDate: string): boolean { + if (!event.recurrence) return false + + const target = new Date(targetDate) + const eventStart = new Date(event.start.split('T')[0]) // Get date part only + + // Check if target date is before the event start date + if (target < eventStart) return false + + // Check if target date is after the event end date (if specified) + if (event.recurrence.endDate) { + const endDate = new Date(event.recurrence.endDate) + if (target > endDate) return false + } + + // Check frequency-specific rules + if (event.recurrence.frequency === 'daily') { + // Daily events occur every day within the range + return true + } else if (event.recurrence.frequency === 'weekly') { + // Weekly events occur on specific day of week + const targetDayOfWeek = target.toLocaleDateString('en-US', { weekday: 'long' }).toLowerCase() + const eventDayOfWeek = event.recurrence.dayOfWeek?.toLowerCase() + return targetDayOfWeek === eventDayOfWeek + } + + return false + } + /** * Get events for today, optionally filtered by user participation */ getTodaysEvents(userPubkey?: string): ScheduledEvent[] { const today = new Date().toISOString().split('T')[0] - let events = this.getEventsForDate(today) + + // Get one-time events for today + const oneTimeEvents = this.getEventsForDate(today) + + // Get all events and check for recurring events that occur today + const allEvents = this.getScheduledEvents() + const recurringEventsToday = allEvents.filter(event => + event.recurrence && this.doesRecurringEventOccurOnDate(event, today) + ) + + // Combine one-time and recurring events + let events = [...oneTimeEvents, ...recurringEventsToday] // Filter events based on participation (if user pubkey provided) if (userPubkey) { @@ -217,24 +289,25 @@ export class ScheduledEventService extends BaseService { } /** - * Get completion status for an event + * Get completion status for an event (optionally for a specific occurrence) */ - getCompletion(eventAddress: string): EventCompletion | undefined { - return this._completions.get(eventAddress) + getCompletion(eventAddress: string, occurrence?: string): EventCompletion | undefined { + const completionKey = occurrence ? `${eventAddress}:${occurrence}` : eventAddress + return this._completions.get(completionKey) } /** - * Check if an event is completed + * Check if an event is completed (optionally for a specific occurrence) */ - isCompleted(eventAddress: string): boolean { - const completion = this.getCompletion(eventAddress) + isCompleted(eventAddress: string, occurrence?: string): boolean { + const completion = this.getCompletion(eventAddress, occurrence) return completion?.completed || false } /** - * Mark an event as complete + * Mark an event as complete (optionally for a specific occurrence) */ - async completeEvent(event: ScheduledEvent, notes: string = ''): Promise { + async completeEvent(event: ScheduledEvent, notes: string = '', occurrence?: string): Promise { if (!this.authService?.isAuthenticated?.value) { throw new Error('Must be authenticated to complete events') } @@ -254,15 +327,22 @@ export class ScheduledEventService extends BaseService { const eventAddress = `31922:${event.pubkey}:${event.dTag}` // Create RSVP/completion event (NIP-52) + const tags: string[][] = [ + ['a', eventAddress], + ['status', 'accepted'], + ['completed', 'true'], + ['completed_at', Math.floor(Date.now() / 1000).toString()] + ] + + // Add occurrence tag if provided (for recurring events) + if (occurrence) { + tags.push(['occurrence', occurrence]) + } + const eventTemplate: EventTemplate = { kind: 31925, // Calendar Event RSVP content: notes, - tags: [ - ['a', eventAddress], - ['status', 'accepted'], - ['completed', 'true'], - ['completed_at', Math.floor(Date.now() / 1000).toString()] - ], + tags, created_at: Math.floor(Date.now() / 1000) } @@ -290,7 +370,7 @@ export class ScheduledEventService extends BaseService { /** * Uncomplete an event (publish new RSVP with completed=false) */ - async uncompleteEvent(event: ScheduledEvent): Promise { + async uncompleteEvent(event: ScheduledEvent, occurrence?: string): Promise { if (!this.authService?.isAuthenticated?.value) { throw new Error('Must be authenticated to uncomplete events') } @@ -310,14 +390,21 @@ export class ScheduledEventService extends BaseService { const eventAddress = `31922:${event.pubkey}:${event.dTag}` // Create RSVP event with completed=false + const tags: string[][] = [ + ['a', eventAddress], + ['status', 'tentative'], + ['completed', 'false'] + ] + + // Add occurrence tag if provided (for recurring events) + if (occurrence) { + tags.push(['occurrence', occurrence]) + } + const eventTemplate: EventTemplate = { kind: 31925, content: '', - tags: [ - ['a', eventAddress], - ['status', 'tentative'], - ['completed', 'false'] - ], + tags, created_at: Math.floor(Date.now() / 1000) } From 83a87b2da6d81e3716a869b6670f8a6f379a7de8 Mon Sep 17 00:00:00 2001 From: padreug Date: Thu, 23 Oct 2025 03:56:03 +0200 Subject: [PATCH 06/10] Filters one-time events to avoid duplicates Ensures that one-time events exclude recurring events, preventing duplicate entries. This resolves an issue where recurring events were incorrectly included in the list of one-time events, leading to events being displayed multiple times. --- src/modules/nostr-feed/services/ScheduledEventService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/nostr-feed/services/ScheduledEventService.ts b/src/modules/nostr-feed/services/ScheduledEventService.ts index 17d9f43..09faee6 100644 --- a/src/modules/nostr-feed/services/ScheduledEventService.ts +++ b/src/modules/nostr-feed/services/ScheduledEventService.ts @@ -256,8 +256,8 @@ export class ScheduledEventService extends BaseService { getTodaysEvents(userPubkey?: string): ScheduledEvent[] { const today = new Date().toISOString().split('T')[0] - // Get one-time events for today - const oneTimeEvents = this.getEventsForDate(today) + // Get one-time events for today (exclude recurring events to avoid duplicates) + const oneTimeEvents = this.getEventsForDate(today).filter(event => !event.recurrence) // Get all events and check for recurring events that occur today const allEvents = this.getScheduledEvents() From 937a0e90750d7be9455e6f79d8f8ee14cb5e8a71 Mon Sep 17 00:00:00 2001 From: padreug Date: Thu, 23 Oct 2025 15:05:58 +0200 Subject: [PATCH 07/10] Adds date navigation to scheduled events Implements date navigation for scheduled events, allowing users to view events for different days. This change replaces the static "Today's Events" section with a dynamic date selector. It introduces buttons for navigating to the previous and next days, as well as a "Today" button to return to the current date. A date display shows the selected date, and a message indicates when there are no scheduled events for a given day. --- .../nostr-feed/components/NostrFeed.vue | 123 ++++++++++++++++-- .../composables/useScheduledEvents.ts | 10 ++ .../services/ScheduledEventService.ts | 27 ++-- 3 files changed, 137 insertions(+), 23 deletions(-) diff --git a/src/modules/nostr-feed/components/NostrFeed.vue b/src/modules/nostr-feed/components/NostrFeed.vue index 7f22c2b..0c2ae18 100644 --- a/src/modules/nostr-feed/components/NostrFeed.vue +++ b/src/modules/nostr-feed/components/NostrFeed.vue @@ -9,7 +9,7 @@ import { DialogHeader, DialogTitle, } from '@/components/ui/dialog' -import { Megaphone, RefreshCw, AlertCircle } from 'lucide-vue-next' +import { Megaphone, RefreshCw, AlertCircle, ChevronLeft, ChevronRight } from 'lucide-vue-next' import { useFeed } from '../composables/useFeed' import { useProfiles } from '../composables/useProfiles' import { useReactions } from '../composables/useReactions' @@ -99,10 +99,66 @@ const { getDisplayName, fetchProfiles } = useProfiles() const { getEventReactions, subscribeToReactions, toggleLike } = useReactions() // Use scheduled events service -const { getTodaysEvents, getCompletion, toggleComplete, allCompletions } = useScheduledEvents() +const { getEventsForSpecificDate, getCompletion, toggleComplete, allCompletions } = useScheduledEvents() -// Get today's scheduled events (reactive) -const todaysScheduledEvents = computed(() => getTodaysEvents()) +// Selected date for viewing events (defaults to today) +const selectedDate = ref(new Date().toISOString().split('T')[0]) + +// Get scheduled events for the selected date (reactive) +const scheduledEventsForDate = computed(() => getEventsForSpecificDate(selectedDate.value)) + +// Navigate to previous day +function goToPreviousDay() { + const date = new Date(selectedDate.value) + date.setDate(date.getDate() - 1) + selectedDate.value = date.toISOString().split('T')[0] +} + +// Navigate to next day +function goToNextDay() { + const date = new Date(selectedDate.value) + date.setDate(date.getDate() + 1) + selectedDate.value = date.toISOString().split('T')[0] +} + +// Go back to today +function goToToday() { + selectedDate.value = new Date().toISOString().split('T')[0] +} + +// Check if selected date is today +const isToday = computed(() => { + const today = new Date().toISOString().split('T')[0] + return selectedDate.value === today +}) + +// Format date for display +const dateDisplayText = computed(() => { + const today = new Date().toISOString().split('T')[0] + const yesterday = new Date() + yesterday.setDate(yesterday.getDate() - 1) + const yesterdayStr = yesterday.toISOString().split('T')[0] + const tomorrow = new Date() + tomorrow.setDate(tomorrow.getDate() + 1) + const tomorrowStr = tomorrow.toISOString().split('T')[0] + + if (selectedDate.value === today) { + return "Today's Events" + } else if (selectedDate.value === yesterdayStr) { + return "Yesterday's Events" + } else if (selectedDate.value === tomorrowStr) { + return "Tomorrow's Events" + } else { + // Format as "Events for Mon, Jan 15" + const date = new Date(selectedDate.value + 'T00:00:00') + const formatted = date.toLocaleDateString('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric' + }) + return `Events for ${formatted}` + } +}) // Watch for new posts and fetch their profiles and reactions watch(notes, async (newNotes) => { @@ -119,12 +175,12 @@ watch(notes, async (newNotes) => { }, { immediate: true }) // Watch for scheduled events and fetch profiles for event authors and completers -watch(todaysScheduledEvents, async (events) => { +watch(scheduledEventsForDate, async (events) => { if (events.length > 0) { const pubkeys = new Set() // Add event authors - events.forEach(event => { + events.forEach((event: ScheduledEvent) => { pubkeys.add(event.pubkey) // Add completer pubkey if event is completed @@ -421,14 +477,50 @@ function cancelDelete() {
- -
-

- 📅 Today's Events -

-
+ +
+
+ + + + +
+

+ 📅 {{ dateDisplayText }} +

+ +
+ + + +
+ + +
+
+ No events scheduled for this day +
-

+

💬 Posts

{ + if (!scheduledEventService) return [] + return scheduledEventService.getEventsForSpecificDate(date, currentUserPubkey.value) + } + /** * Get today's scheduled events (filtered by current user participation) */ @@ -141,6 +150,7 @@ export function useScheduledEvents() { // Methods getScheduledEvents, getEventsForDate, + getEventsForSpecificDate, getTodaysEvents, getCompletion, isCompleted, diff --git a/src/modules/nostr-feed/services/ScheduledEventService.ts b/src/modules/nostr-feed/services/ScheduledEventService.ts index 09faee6..21af01f 100644 --- a/src/modules/nostr-feed/services/ScheduledEventService.ts +++ b/src/modules/nostr-feed/services/ScheduledEventService.ts @@ -251,22 +251,24 @@ export class ScheduledEventService extends BaseService { } /** - * Get events for today, optionally filtered by user participation + * Get events for a specific date, optionally filtered by user participation + * @param date - ISO date string (YYYY-MM-DD). Defaults to today. + * @param userPubkey - Optional user pubkey to filter by participation */ - getTodaysEvents(userPubkey?: string): ScheduledEvent[] { - const today = new Date().toISOString().split('T')[0] + getEventsForSpecificDate(date?: string, userPubkey?: string): ScheduledEvent[] { + const targetDate = date || new Date().toISOString().split('T')[0] - // Get one-time events for today (exclude recurring events to avoid duplicates) - const oneTimeEvents = this.getEventsForDate(today).filter(event => !event.recurrence) + // Get one-time events for the date (exclude recurring events to avoid duplicates) + const oneTimeEvents = this.getEventsForDate(targetDate).filter(event => !event.recurrence) - // Get all events and check for recurring events that occur today + // Get all events and check for recurring events that occur on this date const allEvents = this.getScheduledEvents() - const recurringEventsToday = allEvents.filter(event => - event.recurrence && this.doesRecurringEventOccurOnDate(event, today) + const recurringEventsOnDate = allEvents.filter(event => + event.recurrence && this.doesRecurringEventOccurOnDate(event, targetDate) ) // Combine one-time and recurring events - let events = [...oneTimeEvents, ...recurringEventsToday] + let events = [...oneTimeEvents, ...recurringEventsOnDate] // Filter events based on participation (if user pubkey provided) if (userPubkey) { @@ -288,6 +290,13 @@ export class ScheduledEventService extends BaseService { return events } + /** + * Get events for today, optionally filtered by user participation + */ + getTodaysEvents(userPubkey?: string): ScheduledEvent[] { + return this.getEventsForSpecificDate(undefined, userPubkey) + } + /** * Get completion status for an event (optionally for a specific occurrence) */ From 7e698d2113b9fd50a77ddb5c641ecce6502b43cf Mon Sep 17 00:00:00 2001 From: padreug Date: Thu, 23 Oct 2025 15:44:56 +0200 Subject: [PATCH 08/10] FIX: Show events even if no posts Moves the "no posts" message to only display when there are no posts and no scheduled events. Also, ensures "end of feed" message is displayed only when there are posts to show. --- .../nostr-feed/components/NostrFeed.vue | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/modules/nostr-feed/components/NostrFeed.vue b/src/modules/nostr-feed/components/NostrFeed.vue index 0c2ae18..3afdbdc 100644 --- a/src/modules/nostr-feed/components/NostrFeed.vue +++ b/src/modules/nostr-feed/components/NostrFeed.vue @@ -464,17 +464,6 @@ function cancelDelete() {

- -
-
- - No posts yet -
-

- Check back later for community updates. -

-
-
@@ -559,8 +548,19 @@ function cancelDelete() { />
+ +
+
+ + No posts yet +
+

+ Check back later for community updates. +

+
+ -
+

🐢

From ffe9a10240f8f26ba0629eab197c1d2b49fe9a47 Mon Sep 17 00:00:00 2001 From: padreug Date: Thu, 30 Oct 2025 22:48:58 +0100 Subject: [PATCH 09/10] Removes redundant admin badge Removes the admin badge from the scheduled event card. The badge was deemed unnecessary as the information it conveyed is already implicitly clear. --- src/modules/nostr-feed/components/ScheduledEventCard.vue | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/modules/nostr-feed/components/ScheduledEventCard.vue b/src/modules/nostr-feed/components/ScheduledEventCard.vue index fbe50b9..dfc48af 100644 --- a/src/modules/nostr-feed/components/ScheduledEventCard.vue +++ b/src/modules/nostr-feed/components/ScheduledEventCard.vue @@ -169,11 +169,6 @@ function cancelMarkComplete() { 🔄 - - - - Admin -
From c8f16eda425c397142a1864e9ba390aefe074281 Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 31 Oct 2025 21:04:22 +0100 Subject: [PATCH 10/10] Improves task display and spacing Refines the presentation of scheduled events by adjusting spacing and text displayed when there are no tasks. This enhances visual clarity and provides a more user-friendly experience. --- src/modules/nostr-feed/components/NostrFeed.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/nostr-feed/components/NostrFeed.vue b/src/modules/nostr-feed/components/NostrFeed.vue index 3afdbdc..2d0aa4c 100644 --- a/src/modules/nostr-feed/components/NostrFeed.vue +++ b/src/modules/nostr-feed/components/NostrFeed.vue @@ -467,7 +467,7 @@ function cancelDelete() {
-
+
-
- No events scheduled for this day +
+ {{ isToday ? 'no tasks today' : 'no tasks for this day' }}