diff --git a/src/modules/nostr-feed/components/NostrFeed.vue b/src/modules/nostr-feed/components/NostrFeed.vue
index 1061ea5..2d0aa4c 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
@@ -200,10 +256,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)
@@ -408,27 +464,52 @@ function cancelDelete() {
-
-
-
-
- No posts yet
-
-
- Check back later for community updates.
-
-
-
-
-
-
- 📅 Today's Events
-
-
+
+
+
+
+
+
+
+
+
+ 📅 {{ dateDisplayText }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ isToday ? 'no tasks today' : 'no tasks for this day' }}
+
-
+
💬 Posts
+
+
+
+
+ No posts yet
+
+
+ Check back later for community updates.
+
+
+
-
diff --git a/src/modules/nostr-feed/components/ScheduledEventCard.vue b/src/modules/nostr-feed/components/ScheduledEventCard.vue
index 5acfa51..dfc48af 100644
--- a/src/modules/nostr-feed/components/ScheduledEventCard.vue
+++ b/src/modules/nostr-feed/components/ScheduledEventCard.vue
@@ -10,18 +10,23 @@ 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'
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
(), {
@@ -33,17 +38,23 @@ 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}`)
+// 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')
@@ -107,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
}
@@ -116,81 +127,56 @@ function confirmMarkComplete() {
function cancelMarkComplete() {
showConfirmDialog.value = false
}
-
-// Toggle expanded/collapsed state
-function toggleExpanded() {
- isExpanded.value = !isExpanded.value
-}
-
-
-
-
-
-
-
- {{ formattedTimeRange || formattedDate }}
-
-
-
-
- {{ event.title }}
-
-
-
-
-
-
-
-
-
- ✓
-
-
-
-
- Admin
-
-
-
-
-
-
-
-
-
-
-
- {{ event.title }}
-
-
- Admin
-
-
+
+
+
+
+
+
+
+ {{ formattedTimeRange || formattedDate }}
+
+
+ {{ event.title }}
+
+
+
+
+
+
+
+
+
+ ✓ {{ getDisplayName(getCompletion(eventAddress, occurrence)!.pubkey) }}
+
+
+
+
+ 🔄
+
+
+
+
+
+
+
+
+
@@ -215,9 +201,9 @@ function toggleExpanded() {
-
- ✓ Completed by {{ getDisplayName(getCompletion(eventAddress)!.pubkey) }}
-
- {{ getCompletion(eventAddress)!.notes }}
+
+ ✓ Completed by {{ getDisplayName(getCompletion(eventAddress, occurrence)!.pubkey) }}
+ - {{ getCompletion(eventAddress, occurrence)!.notes }}
@@ -238,22 +224,23 @@ function toggleExpanded() {
-
+
-
-
-
+
+
+
+
diff --git a/src/modules/nostr-feed/composables/useScheduledEvents.ts b/src/modules/nostr-feed/composables/useScheduledEvents.ts
index ae79702..e56c002 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,20 @@ export function useScheduledEvents() {
}
/**
- * Get today's scheduled events
+ * Get events for a specific date (filtered by current user participation)
+ * @param date - ISO date string (YYYY-MM-DD). Defaults to today.
+ */
+ const getEventsForSpecificDate = (date?: string): ScheduledEvent[] => {
+ if (!scheduledEventService) return []
+ return scheduledEventService.getEventsForSpecificDate(date, currentUserPubkey.value)
+ }
+
+ /**
+ * Get today's scheduled events (filtered by current user participation)
*/
const getTodaysEvents = (): ScheduledEvent[] => {
if (!scheduledEventService) return []
- return scheduledEventService.getTodaysEvents()
+ return scheduledEventService.getTodaysEvents(currentUserPubkey.value)
}
/**
@@ -51,10 +65,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')
@@ -64,16 +78,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) {
@@ -136,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 19bda58..21af01f 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
@@ -16,13 +22,16 @@ 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[][]
+ 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
@@ -79,6 +88,27 @@ 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'
+ }))
+
+ // 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
@@ -99,8 +129,10 @@ export class ScheduledEventService extends BaseService {
location,
status,
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)
@@ -129,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
@@ -140,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,
@@ -148,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) {
@@ -181,32 +219,104 @@ export class ScheduledEventService extends BaseService {
}
/**
- * Get events for today
+ * Check if a recurring event occurs on a specific date
*/
- getTodaysEvents(): ScheduledEvent[] {
- const today = new Date().toISOString().split('T')[0]
- return this.getEventsForDate(today)
+ 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 completion status for an event
+ * 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
*/
- getCompletion(eventAddress: string): EventCompletion | undefined {
- return this._completions.get(eventAddress)
+ getEventsForSpecificDate(date?: string, userPubkey?: string): ScheduledEvent[] {
+ const targetDate = date || new Date().toISOString().split('T')[0]
+
+ // 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 on this date
+ const allEvents = this.getScheduledEvents()
+ const recurringEventsOnDate = allEvents.filter(event =>
+ event.recurrence && this.doesRecurringEventOccurOnDate(event, targetDate)
+ )
+
+ // Combine one-time and recurring events
+ let events = [...oneTimeEvents, ...recurringEventsOnDate]
+
+ // 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
+
+ // 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
}
/**
- * Check if an event is completed
+ * Get events for today, optionally filtered by user participation
*/
- isCompleted(eventAddress: string): boolean {
- const completion = this.getCompletion(eventAddress)
+ getTodaysEvents(userPubkey?: string): ScheduledEvent[] {
+ return this.getEventsForSpecificDate(undefined, userPubkey)
+ }
+
+ /**
+ * Get completion status for an event (optionally for a specific occurrence)
+ */
+ getCompletion(eventAddress: string, occurrence?: string): EventCompletion | undefined {
+ const completionKey = occurrence ? `${eventAddress}:${occurrence}` : eventAddress
+ return this._completions.get(completionKey)
+ }
+
+ /**
+ * Check if an event is completed (optionally for a specific occurrence)
+ */
+ 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')
}
@@ -226,15 +336,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)
}
@@ -262,7 +379,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')
}
@@ -282,14 +399,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)
}