Compare commits
10 commits
5171b24ba3
...
c8f16eda42
| Author | SHA1 | Date | |
|---|---|---|---|
| c8f16eda42 | |||
| ffe9a10240 | |||
| 7e698d2113 | |||
| 937a0e9075 | |||
| 83a87b2da6 | |||
| 0f0eae8800 | |||
| 2cf737213b | |||
| 62161dd000 | |||
| 93e3e0a1ca | |||
| b023d54264 |
4 changed files with 386 additions and 165 deletions
|
|
@ -9,7 +9,7 @@ import {
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog'
|
} 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 { useFeed } from '../composables/useFeed'
|
||||||
import { useProfiles } from '../composables/useProfiles'
|
import { useProfiles } from '../composables/useProfiles'
|
||||||
import { useReactions } from '../composables/useReactions'
|
import { useReactions } from '../composables/useReactions'
|
||||||
|
|
@ -99,10 +99,66 @@ const { getDisplayName, fetchProfiles } = useProfiles()
|
||||||
const { getEventReactions, subscribeToReactions, toggleLike } = useReactions()
|
const { getEventReactions, subscribeToReactions, toggleLike } = useReactions()
|
||||||
|
|
||||||
// Use scheduled events service
|
// Use scheduled events service
|
||||||
const { getTodaysEvents, getCompletion, toggleComplete, allCompletions } = useScheduledEvents()
|
const { getEventsForSpecificDate, getCompletion, toggleComplete, allCompletions } = useScheduledEvents()
|
||||||
|
|
||||||
// Get today's scheduled events (reactive)
|
// Selected date for viewing events (defaults to today)
|
||||||
const todaysScheduledEvents = computed(() => getTodaysEvents())
|
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 for new posts and fetch their profiles and reactions
|
||||||
watch(notes, async (newNotes) => {
|
watch(notes, async (newNotes) => {
|
||||||
|
|
@ -119,12 +175,12 @@ watch(notes, async (newNotes) => {
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
// Watch for scheduled events and fetch profiles for event authors and completers
|
// Watch for scheduled events and fetch profiles for event authors and completers
|
||||||
watch(todaysScheduledEvents, async (events) => {
|
watch(scheduledEventsForDate, async (events) => {
|
||||||
if (events.length > 0) {
|
if (events.length > 0) {
|
||||||
const pubkeys = new Set<string>()
|
const pubkeys = new Set<string>()
|
||||||
|
|
||||||
// Add event authors
|
// Add event authors
|
||||||
events.forEach(event => {
|
events.forEach((event: ScheduledEvent) => {
|
||||||
pubkeys.add(event.pubkey)
|
pubkeys.add(event.pubkey)
|
||||||
|
|
||||||
// Add completer pubkey if event is completed
|
// Add completer pubkey if event is completed
|
||||||
|
|
@ -200,10 +256,10 @@ async function onToggleLike(note: FeedPost) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle scheduled event completion toggle
|
// Handle scheduled event completion toggle
|
||||||
async function onToggleComplete(event: ScheduledEvent) {
|
async function onToggleComplete(event: ScheduledEvent, occurrence?: string) {
|
||||||
console.log('🎯 NostrFeed: onToggleComplete called for event:', event.title)
|
console.log('🎯 NostrFeed: onToggleComplete called for event:', event.title, 'occurrence:', occurrence)
|
||||||
try {
|
try {
|
||||||
await toggleComplete(event)
|
await toggleComplete(event, occurrence)
|
||||||
console.log('✅ NostrFeed: toggleComplete succeeded')
|
console.log('✅ NostrFeed: toggleComplete succeeded')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ NostrFeed: Failed to toggle event completion:', error)
|
console.error('❌ NostrFeed: Failed to toggle event completion:', error)
|
||||||
|
|
@ -408,27 +464,52 @@ function cancelDelete() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- No Posts -->
|
|
||||||
<div v-else-if="threadedPosts.length === 0" class="text-center py-8 px-4">
|
|
||||||
<div class="flex items-center justify-center gap-2 text-muted-foreground mb-4">
|
|
||||||
<Megaphone class="h-5 w-5" />
|
|
||||||
<span>No posts yet</span>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-muted-foreground">
|
|
||||||
Check back later for community updates.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Posts List - Natural flow without internal scrolling -->
|
<!-- Posts List - Natural flow without internal scrolling -->
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<!-- Today's Scheduled Events Section -->
|
<!-- Scheduled Events Section with Date Navigation -->
|
||||||
<div v-if="todaysScheduledEvents.length > 0" class="mb-6 md:mb-8">
|
<div class="my-2 md:my-4">
|
||||||
<h3 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide px-4 md:px-0 mb-3">
|
<div class="flex items-center justify-between px-4 md:px-0 mb-3">
|
||||||
📅 Today's Events
|
<!-- Left Arrow -->
|
||||||
</h3>
|
<Button
|
||||||
<div class="md:space-y-3">
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
@click="goToPreviousDay"
|
||||||
|
>
|
||||||
|
<ChevronLeft class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- Date Header with Today Button -->
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<h3 class="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
||||||
|
📅 {{ dateDisplayText }}
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
v-if="!isToday"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
class="h-6 text-xs"
|
||||||
|
@click="goToToday"
|
||||||
|
>
|
||||||
|
Today
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Arrow -->
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
class="h-8 w-8"
|
||||||
|
@click="goToNextDay"
|
||||||
|
>
|
||||||
|
<ChevronRight class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Events List or Empty State -->
|
||||||
|
<div v-if="scheduledEventsForDate.length > 0" class="md:space-y-3">
|
||||||
<ScheduledEventCard
|
<ScheduledEventCard
|
||||||
v-for="event in todaysScheduledEvents"
|
v-for="event in scheduledEventsForDate"
|
||||||
:key="`${event.pubkey}:${event.dTag}`"
|
:key="`${event.pubkey}:${event.dTag}`"
|
||||||
:event="event"
|
:event="event"
|
||||||
:get-display-name="getDisplayName"
|
:get-display-name="getDisplayName"
|
||||||
|
|
@ -437,11 +518,14 @@ function cancelDelete() {
|
||||||
@toggle-complete="onToggleComplete"
|
@toggle-complete="onToggleComplete"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="text-center py-3 text-muted-foreground text-sm px-4">
|
||||||
|
{{ isToday ? 'no tasks today' : 'no tasks for this day' }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Posts Section -->
|
<!-- Posts Section -->
|
||||||
<div v-if="threadedPosts.length > 0" class="md:space-y-4 md:py-4">
|
<div v-if="threadedPosts.length > 0" class="md:space-y-4 md:py-4">
|
||||||
<h3 v-if="todaysScheduledEvents.length > 0" class="text-sm font-semibold text-muted-foreground uppercase tracking-wide px-4 md:px-0 mb-3 mt-6">
|
<h3 v-if="scheduledEventsForDate.length > 0" class="text-sm font-semibold text-muted-foreground uppercase tracking-wide px-4 md:px-0 mb-3 mt-6">
|
||||||
💬 Posts
|
💬 Posts
|
||||||
</h3>
|
</h3>
|
||||||
<ThreadedPost
|
<ThreadedPost
|
||||||
|
|
@ -464,8 +548,19 @@ function cancelDelete() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- No Posts Message (show whenever there are no posts, regardless of events) -->
|
||||||
|
<div v-else class="text-center py-8 px-4">
|
||||||
|
<div class="flex items-center justify-center gap-2 text-muted-foreground mb-4">
|
||||||
|
<Megaphone class="h-5 w-5" />
|
||||||
|
<span>No posts yet</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
Check back later for community updates.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- End of feed message -->
|
<!-- End of feed message -->
|
||||||
<div class="text-center py-6 text-md text-muted-foreground">
|
<div v-if="threadedPosts.length > 0" class="text-center py-6 text-md text-muted-foreground">
|
||||||
<p>🐢</p>
|
<p>🐢</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,23 @@ import {
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from '@/components/ui/collapsible'
|
||||||
import { Calendar, MapPin, Clock, CheckCircle } from 'lucide-vue-next'
|
import { Calendar, MapPin, Clock, CheckCircle } from 'lucide-vue-next'
|
||||||
import type { ScheduledEvent, EventCompletion } from '../services/ScheduledEventService'
|
import type { ScheduledEvent, EventCompletion } from '../services/ScheduledEventService'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
event: ScheduledEvent
|
event: ScheduledEvent
|
||||||
getDisplayName: (pubkey: string) => string
|
getDisplayName: (pubkey: string) => string
|
||||||
getCompletion: (eventAddress: string) => EventCompletion | undefined
|
getCompletion: (eventAddress: string, occurrence?: string) => EventCompletion | undefined
|
||||||
adminPubkeys?: string[]
|
adminPubkeys?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'toggle-complete', event: ScheduledEvent): void
|
(e: 'toggle-complete', event: ScheduledEvent, occurrence?: string): void
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
@ -33,17 +38,23 @@ const emit = defineEmits<Emits>()
|
||||||
// Confirmation dialog state
|
// Confirmation dialog state
|
||||||
const showConfirmDialog = ref(false)
|
const showConfirmDialog = ref(false)
|
||||||
|
|
||||||
// Collapsed state (collapsed by default)
|
|
||||||
const isExpanded = ref(false)
|
|
||||||
|
|
||||||
// Event address for tracking completion
|
// Event address for tracking completion
|
||||||
const eventAddress = computed(() => `31922:${props.event.pubkey}:${props.event.dTag}`)
|
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
|
// Check if this is an admin event
|
||||||
const isAdminEvent = computed(() => props.adminPubkeys.includes(props.event.pubkey))
|
const isAdminEvent = computed(() => props.adminPubkeys.includes(props.event.pubkey))
|
||||||
|
|
||||||
// Check if event is completed - call function directly
|
// Check if event is completed - call function with occurrence for recurring events
|
||||||
const isCompleted = computed(() => props.getCompletion(eventAddress.value)?.completed || false)
|
const isCompleted = computed(() => props.getCompletion(eventAddress.value, occurrence.value)?.completed || false)
|
||||||
|
|
||||||
// Check if event is completable (task type)
|
// Check if event is completable (task type)
|
||||||
const isCompletable = computed(() => props.event.eventType === 'task')
|
const isCompletable = computed(() => props.event.eventType === 'task')
|
||||||
|
|
@ -107,8 +118,8 @@ function handleMarkComplete() {
|
||||||
|
|
||||||
// Confirm and execute mark complete
|
// Confirm and execute mark complete
|
||||||
function confirmMarkComplete() {
|
function confirmMarkComplete() {
|
||||||
console.log('✅ Confirmed mark complete for event:', props.event.title)
|
console.log('✅ Confirmed mark complete for event:', props.event.title, 'occurrence:', occurrence.value)
|
||||||
emit('toggle-complete', props.event)
|
emit('toggle-complete', props.event, occurrence.value)
|
||||||
showConfirmDialog.value = false
|
showConfirmDialog.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,81 +127,56 @@ function confirmMarkComplete() {
|
||||||
function cancelMarkComplete() {
|
function cancelMarkComplete() {
|
||||||
showConfirmDialog.value = false
|
showConfirmDialog.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle expanded/collapsed state
|
|
||||||
function toggleExpanded() {
|
|
||||||
isExpanded.value = !isExpanded.value
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="border-b md:border md:rounded-lg bg-card transition-all"
|
<Collapsible class="border-b md:border md:rounded-lg bg-card transition-all"
|
||||||
:class="{ 'opacity-60': isCompletable && isCompleted }">
|
:class="{ 'opacity-60': isCompletable && isCompleted }">
|
||||||
|
<!-- Collapsed View (Trigger) -->
|
||||||
<!-- Collapsed View (Default) -->
|
<CollapsibleTrigger as-child>
|
||||||
<div v-if="!isExpanded"
|
<div class="flex items-center gap-3 p-3 md:p-4 cursor-pointer hover:bg-accent/50 transition-colors">
|
||||||
class="flex items-center gap-3 p-3 md:p-4">
|
<!-- Time -->
|
||||||
<!-- Time -->
|
<div class="flex items-center gap-1.5 text-sm text-muted-foreground shrink-0">
|
||||||
<div @click="toggleExpanded" class="flex items-center gap-1.5 text-sm text-muted-foreground shrink-0 cursor-pointer">
|
<Clock class="h-3.5 w-3.5" />
|
||||||
<Clock class="h-3.5 w-3.5" />
|
<span class="font-medium">{{ formattedTimeRange || formattedDate }}</span>
|
||||||
<span class="font-medium">{{ formattedTimeRange || formattedDate }}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Title -->
|
|
||||||
<h3 @click="toggleExpanded"
|
|
||||||
class="font-semibold text-sm md:text-base flex-1 truncate cursor-pointer hover:text-foreground/80 transition-colors"
|
|
||||||
:class="{ 'line-through': isCompletable && isCompleted }">
|
|
||||||
{{ event.title }}
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<!-- Badges and Actions -->
|
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
|
||||||
<!-- Mark Complete Button (for uncompleted tasks) -->
|
|
||||||
<Button
|
|
||||||
v-if="isCompletable && !isCompleted"
|
|
||||||
@click.stop="handleMarkComplete"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-7 w-7 p-0"
|
|
||||||
>
|
|
||||||
<CheckCircle class="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<!-- Completed Badge -->
|
|
||||||
<Badge v-if="isCompletable && isCompleted" variant="secondary" class="text-xs">
|
|
||||||
✓
|
|
||||||
</Badge>
|
|
||||||
|
|
||||||
<!-- Admin Badge -->
|
|
||||||
<Badge v-if="isAdminEvent" variant="secondary" class="text-xs">
|
|
||||||
Admin
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Expanded View -->
|
|
||||||
<div v-else class="p-4 md:p-6">
|
|
||||||
<!-- Event Details -->
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<!-- Title and badges with close button -->
|
|
||||||
<div class="flex items-start gap-2 mb-2 flex-wrap">
|
|
||||||
<h3 class="font-semibold text-base md:text-lg flex-1"
|
|
||||||
:class="{ 'line-through': isCompletable && isCompleted }">
|
|
||||||
{{ event.title }}
|
|
||||||
</h3>
|
|
||||||
<Badge v-if="isAdminEvent" variant="secondary" class="shrink-0">
|
|
||||||
Admin
|
|
||||||
</Badge>
|
|
||||||
<Button
|
|
||||||
@click="toggleExpanded"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
class="h-6 w-6 p-0 shrink-0"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<h3 class="font-semibold text-sm md:text-base flex-1 truncate"
|
||||||
|
:class="{ 'line-through': isCompletable && isCompleted }">
|
||||||
|
{{ event.title }}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Badges and Actions -->
|
||||||
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
|
<!-- Mark Complete Button (for uncompleted tasks) -->
|
||||||
|
<Button
|
||||||
|
v-if="isCompletable && !isCompleted"
|
||||||
|
@click.stop="handleMarkComplete"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
class="h-7 w-7 p-0"
|
||||||
|
>
|
||||||
|
<CheckCircle class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<!-- Completed Badge with completer name -->
|
||||||
|
<Badge v-if="isCompletable && isCompleted && getCompletion(eventAddress, occurrence)" variant="secondary" class="text-xs">
|
||||||
|
✓ {{ getDisplayName(getCompletion(eventAddress, occurrence)!.pubkey) }}
|
||||||
|
</Badge>
|
||||||
|
|
||||||
|
<!-- Recurring Badge -->
|
||||||
|
<Badge v-if="isRecurring" variant="outline" class="text-xs">
|
||||||
|
🔄
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
|
||||||
|
<!-- Expanded View (Content) -->
|
||||||
|
<CollapsibleContent class="p-4 md:p-6 pt-0">
|
||||||
|
<!-- Event Details -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
<!-- Date/Time -->
|
<!-- Date/Time -->
|
||||||
<div class="flex items-center gap-4 text-sm text-muted-foreground mb-2 flex-wrap">
|
<div class="flex items-center gap-4 text-sm text-muted-foreground mb-2 flex-wrap">
|
||||||
<div class="flex items-center gap-1.5">
|
<div class="flex items-center gap-1.5">
|
||||||
|
|
@ -215,9 +201,9 @@ function toggleExpanded() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Completion info (only for completable events) -->
|
<!-- Completion info (only for completable events) -->
|
||||||
<div v-if="isCompletable && isCompleted && getCompletion(eventAddress)" class="text-xs text-muted-foreground mb-3">
|
<div v-if="isCompletable && isCompleted && getCompletion(eventAddress, occurrence)" class="text-xs text-muted-foreground mb-3">
|
||||||
✓ Completed by {{ getDisplayName(getCompletion(eventAddress)!.pubkey) }}
|
✓ Completed by {{ getDisplayName(getCompletion(eventAddress, occurrence)!.pubkey) }}
|
||||||
<span v-if="getCompletion(eventAddress)!.notes"> - {{ getCompletion(eventAddress)!.notes }}</span>
|
<span v-if="getCompletion(eventAddress, occurrence)!.notes"> - {{ getCompletion(eventAddress, occurrence)!.notes }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Author (if not admin) -->
|
<!-- Author (if not admin) -->
|
||||||
|
|
@ -238,22 +224,23 @@ function toggleExpanded() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CollapsibleContent>
|
||||||
|
|
||||||
<!-- Confirmation Dialog -->
|
</Collapsible>
|
||||||
<Dialog :open="showConfirmDialog" @update:open="(val: boolean) => showConfirmDialog = val">
|
|
||||||
<DialogContent>
|
<!-- Confirmation Dialog -->
|
||||||
<DialogHeader>
|
<Dialog :open="showConfirmDialog" @update:open="(val: boolean) => showConfirmDialog = val">
|
||||||
<DialogTitle>Mark Event as Complete?</DialogTitle>
|
<DialogContent>
|
||||||
<DialogDescription>
|
<DialogHeader>
|
||||||
This will mark "{{ event.title }}" as completed by you. Other users will be able to see that you completed this event.
|
<DialogTitle>Mark Event as Complete?</DialogTitle>
|
||||||
</DialogDescription>
|
<DialogDescription>
|
||||||
</DialogHeader>
|
This will mark "{{ event.title }}" as completed by you. Other users will be able to see that you completed this event.
|
||||||
<DialogFooter>
|
</DialogDescription>
|
||||||
<Button variant="outline" @click="cancelMarkComplete">Cancel</Button>
|
</DialogHeader>
|
||||||
<Button @click="confirmMarkComplete">Mark Complete</Button>
|
<DialogFooter>
|
||||||
</DialogFooter>
|
<Button variant="outline" @click="cancelMarkComplete">Cancel</Button>
|
||||||
</DialogContent>
|
<Button @click="confirmMarkComplete">Mark Complete</Button>
|
||||||
</Dialog>
|
</DialogFooter>
|
||||||
</div>
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
import type { ScheduledEventService, ScheduledEvent, EventCompletion } from '../services/ScheduledEventService'
|
import type { ScheduledEventService, ScheduledEvent, EventCompletion } from '../services/ScheduledEventService'
|
||||||
|
import type { AuthService } from '@/modules/base/auth/auth-service'
|
||||||
import { useToast } from '@/core/composables/useToast'
|
import { useToast } from '@/core/composables/useToast'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -8,8 +9,12 @@ import { useToast } from '@/core/composables/useToast'
|
||||||
*/
|
*/
|
||||||
export function useScheduledEvents() {
|
export function useScheduledEvents() {
|
||||||
const scheduledEventService = injectService<ScheduledEventService>(SERVICE_TOKENS.SCHEDULED_EVENT_SERVICE)
|
const scheduledEventService = injectService<ScheduledEventService>(SERVICE_TOKENS.SCHEDULED_EVENT_SERVICE)
|
||||||
|
const authService = injectService<AuthService>(SERVICE_TOKENS.AUTH_SERVICE)
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
|
// Get current user's pubkey
|
||||||
|
const currentUserPubkey = computed(() => authService?.user.value?.pubkey)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all scheduled events
|
* 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[] => {
|
const getTodaysEvents = (): ScheduledEvent[] => {
|
||||||
if (!scheduledEventService) return []
|
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<void> => {
|
const toggleComplete = async (event: ScheduledEvent, occurrence?: string, notes: string = ''): Promise<void> => {
|
||||||
console.log('🔧 useScheduledEvents: toggleComplete called for event:', event.title)
|
console.log('🔧 useScheduledEvents: toggleComplete called for event:', event.title, 'occurrence:', occurrence)
|
||||||
|
|
||||||
if (!scheduledEventService) {
|
if (!scheduledEventService) {
|
||||||
console.error('❌ useScheduledEvents: Scheduled event service not available')
|
console.error('❌ useScheduledEvents: Scheduled event service not available')
|
||||||
|
|
@ -64,16 +78,16 @@ export function useScheduledEvents() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const eventAddress = `31922:${event.pubkey}:${event.dTag}`
|
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)
|
console.log('📊 useScheduledEvents: Current completion status:', currentlyCompleted)
|
||||||
|
|
||||||
if (currentlyCompleted) {
|
if (currentlyCompleted) {
|
||||||
console.log('⬇️ useScheduledEvents: Marking as incomplete...')
|
console.log('⬇️ useScheduledEvents: Marking as incomplete...')
|
||||||
await scheduledEventService.uncompleteEvent(event)
|
await scheduledEventService.uncompleteEvent(event, occurrence)
|
||||||
toast.success('Event marked as incomplete')
|
toast.success('Event marked as incomplete')
|
||||||
} else {
|
} else {
|
||||||
console.log('⬆️ useScheduledEvents: Marking as complete...')
|
console.log('⬆️ useScheduledEvents: Marking as complete...')
|
||||||
await scheduledEventService.completeEvent(event, notes)
|
await scheduledEventService.completeEvent(event, notes, occurrence)
|
||||||
toast.success('Event completed!')
|
toast.success('Event completed!')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -136,6 +150,7 @@ export function useScheduledEvents() {
|
||||||
// Methods
|
// Methods
|
||||||
getScheduledEvents,
|
getScheduledEvents,
|
||||||
getEventsForDate,
|
getEventsForDate,
|
||||||
|
getEventsForSpecificDate,
|
||||||
getTodaysEvents,
|
getTodaysEvents,
|
||||||
getCompletion,
|
getCompletion,
|
||||||
isCompleted,
|
isCompleted,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@ import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
import { finalizeEvent, type EventTemplate } from 'nostr-tools'
|
import { finalizeEvent, type EventTemplate } from 'nostr-tools'
|
||||||
import type { Event as NostrEvent } 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 {
|
export interface ScheduledEvent {
|
||||||
id: string
|
id: string
|
||||||
pubkey: string
|
pubkey: string
|
||||||
|
|
@ -16,13 +22,16 @@ export interface ScheduledEvent {
|
||||||
location?: string
|
location?: string
|
||||||
status: string
|
status: string
|
||||||
eventType?: string // 'task' for completable events, 'announcement' for informational
|
eventType?: string // 'task' for completable events, 'announcement' for informational
|
||||||
|
participants?: Array<{ pubkey: string; type?: string }> // 'required', 'optional', 'organizer'
|
||||||
content: string
|
content: string
|
||||||
tags: string[][]
|
tags: string[][]
|
||||||
|
recurrence?: RecurrencePattern // Optional: for recurring events
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EventCompletion {
|
export interface EventCompletion {
|
||||||
id: string
|
id: string
|
||||||
eventAddress: string // "31922:pubkey:d-tag"
|
eventAddress: string // "31922:pubkey:d-tag"
|
||||||
|
occurrence?: string // ISO date string for the specific occurrence (YYYY-MM-DD)
|
||||||
pubkey: string // Who completed it
|
pubkey: string // Who completed it
|
||||||
created_at: number
|
created_at: number
|
||||||
completed: boolean
|
completed: boolean
|
||||||
|
|
@ -79,6 +88,27 @@ export class ScheduledEventService extends BaseService {
|
||||||
const status = event.tags.find(tag => tag[0] === 'status')?.[1] || 'pending'
|
const status = event.tags.find(tag => tag[0] === 'status')?.[1] || 'pending'
|
||||||
const eventType = event.tags.find(tag => tag[0] === 'event-type')?.[1]
|
const eventType = event.tags.find(tag => tag[0] === 'event-type')?.[1]
|
||||||
|
|
||||||
|
// Parse participant tags: ["p", "<pubkey>", "<relay-hint>", "<participation-type>"]
|
||||||
|
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) {
|
if (!start) {
|
||||||
console.warn('Scheduled event missing start date:', event.id)
|
console.warn('Scheduled event missing start date:', event.id)
|
||||||
return
|
return
|
||||||
|
|
@ -99,8 +129,10 @@ export class ScheduledEventService extends BaseService {
|
||||||
location,
|
location,
|
||||||
status,
|
status,
|
||||||
eventType,
|
eventType,
|
||||||
|
participants: participants.length > 0 ? participants : undefined,
|
||||||
content: event.content,
|
content: event.content,
|
||||||
tags: event.tags
|
tags: event.tags,
|
||||||
|
recurrence
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store or update the event (replaceable by d-tag)
|
// 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 completed = event.tags.find(tag => tag[0] === 'completed')?.[1] === 'true'
|
||||||
const completedAtTag = event.tags.find(tag => tag[0] === 'completed_at')?.[1]
|
const completedAtTag = event.tags.find(tag => tag[0] === 'completed_at')?.[1]
|
||||||
const completedAt = completedAtTag ? parseInt(completedAtTag) : undefined
|
const completedAt = completedAtTag ? parseInt(completedAtTag) : undefined
|
||||||
|
const occurrence = event.tags.find(tag => tag[0] === 'occurrence')?.[1] // ISO date string
|
||||||
|
|
||||||
console.log('📋 Completion details:', {
|
console.log('📋 Completion details:', {
|
||||||
aTag,
|
aTag,
|
||||||
|
occurrence,
|
||||||
completed,
|
completed,
|
||||||
pubkey: event.pubkey,
|
pubkey: event.pubkey,
|
||||||
eventId: event.id
|
eventId: event.id
|
||||||
|
|
@ -140,6 +174,7 @@ export class ScheduledEventService extends BaseService {
|
||||||
const completion: EventCompletion = {
|
const completion: EventCompletion = {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
eventAddress: aTag,
|
eventAddress: aTag,
|
||||||
|
occurrence,
|
||||||
pubkey: event.pubkey,
|
pubkey: event.pubkey,
|
||||||
created_at: event.created_at,
|
created_at: event.created_at,
|
||||||
completed,
|
completed,
|
||||||
|
|
@ -148,12 +183,15 @@ export class ScheduledEventService extends BaseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store completion (most recent one wins)
|
// 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) {
|
if (!existing || event.created_at > existing.created_at) {
|
||||||
this._completions.set(aTag, completion)
|
this._completions.set(completionKey, completion)
|
||||||
console.log('✅ Stored completion for:', aTag, '- completed:', completed)
|
console.log('✅ Stored completion for:', completionKey, '- completed:', completed)
|
||||||
} else {
|
} else {
|
||||||
console.log('⏭️ Skipped older completion for:', aTag)
|
console.log('⏭️ Skipped older completion for:', completionKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} 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[] {
|
private doesRecurringEventOccurOnDate(event: ScheduledEvent, targetDate: string): boolean {
|
||||||
const today = new Date().toISOString().split('T')[0]
|
if (!event.recurrence) return false
|
||||||
return this.getEventsForDate(today)
|
|
||||||
|
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 {
|
getEventsForSpecificDate(date?: string, userPubkey?: string): ScheduledEvent[] {
|
||||||
return this._completions.get(eventAddress)
|
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 {
|
getTodaysEvents(userPubkey?: string): ScheduledEvent[] {
|
||||||
const completion = this.getCompletion(eventAddress)
|
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
|
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<void> {
|
async completeEvent(event: ScheduledEvent, notes: string = '', occurrence?: string): Promise<void> {
|
||||||
if (!this.authService?.isAuthenticated?.value) {
|
if (!this.authService?.isAuthenticated?.value) {
|
||||||
throw new Error('Must be authenticated to complete events')
|
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}`
|
const eventAddress = `31922:${event.pubkey}:${event.dTag}`
|
||||||
|
|
||||||
// Create RSVP/completion event (NIP-52)
|
// 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 = {
|
const eventTemplate: EventTemplate = {
|
||||||
kind: 31925, // Calendar Event RSVP
|
kind: 31925, // Calendar Event RSVP
|
||||||
content: notes,
|
content: notes,
|
||||||
tags: [
|
tags,
|
||||||
['a', eventAddress],
|
|
||||||
['status', 'accepted'],
|
|
||||||
['completed', 'true'],
|
|
||||||
['completed_at', Math.floor(Date.now() / 1000).toString()]
|
|
||||||
],
|
|
||||||
created_at: Math.floor(Date.now() / 1000)
|
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)
|
* Uncomplete an event (publish new RSVP with completed=false)
|
||||||
*/
|
*/
|
||||||
async uncompleteEvent(event: ScheduledEvent): Promise<void> {
|
async uncompleteEvent(event: ScheduledEvent, occurrence?: string): Promise<void> {
|
||||||
if (!this.authService?.isAuthenticated?.value) {
|
if (!this.authService?.isAuthenticated?.value) {
|
||||||
throw new Error('Must be authenticated to uncomplete events')
|
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}`
|
const eventAddress = `31922:${event.pubkey}:${event.dTag}`
|
||||||
|
|
||||||
// Create RSVP event with completed=false
|
// 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 = {
|
const eventTemplate: EventTemplate = {
|
||||||
kind: 31925,
|
kind: 31925,
|
||||||
content: '',
|
content: '',
|
||||||
tags: [
|
tags,
|
||||||
['a', eventAddress],
|
|
||||||
['status', 'tentative'],
|
|
||||||
['completed', 'false']
|
|
||||||
],
|
|
||||||
created_at: Math.floor(Date.now() / 1000)
|
created_at: Math.floor(Date.now() / 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue