Adds scheduled events to the feed

Implements NIP-52 scheduled events, allowing users to view and interact with calendar events.

A new `ScheduledEventService` is introduced to manage fetching, storing, and completing scheduled events. A new `ScheduledEventCard` component is introduced for displaying the scheduled events.
This commit is contained in:
padreug 2025-10-21 21:58:58 +02:00
parent 875bf50765
commit b15a8c21c0
8 changed files with 716 additions and 1 deletions

View file

@ -0,0 +1,151 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Badge } from '@/components/ui/badge'
import { Checkbox } from '@/components/ui/checkbox'
import { Calendar, MapPin, Clock } from 'lucide-vue-next'
import type { ScheduledEvent, EventCompletion } from '../services/ScheduledEventService'
interface Props {
event: ScheduledEvent
completion?: EventCompletion
getDisplayName: (pubkey: string) => string
adminPubkeys?: string[]
}
interface Emits {
(e: 'toggle-complete', event: ScheduledEvent): void
}
const props = withDefaults(defineProps<Props>(), {
adminPubkeys: () => []
})
const emit = defineEmits<Emits>()
// Check if this is an admin event
const isAdminEvent = computed(() => props.adminPubkeys.includes(props.event.pubkey))
// Check if event is completed
const isCompleted = computed(() => props.completion?.completed || false)
// Format the date/time
const formattedDate = computed(() => {
try {
const date = new Date(props.event.start)
// Check if it's a datetime or just date
if (props.event.start.includes('T')) {
// Full datetime - show date and time
return date.toLocaleString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
})
} else {
// Just date
return date.toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: 'numeric'
})
}
} catch (error) {
return props.event.start
}
})
// Format the time range if end time exists
const formattedTimeRange = computed(() => {
if (!props.event.end || !props.event.start.includes('T')) return null
try {
const start = new Date(props.event.start)
const end = new Date(props.event.end)
const startTime = start.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit'
})
const endTime = end.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit'
})
return `${startTime} - ${endTime}`
} catch (error) {
return null
}
})
// Handle checkbox toggle
function handleToggleComplete() {
emit('toggle-complete', props.event)
}
</script>
<template>
<div class="border-b md:border md:rounded-lg bg-card p-4 md:p-6 transition-all"
:class="{ 'opacity-60': isCompleted }">
<!-- Header -->
<div class="flex items-start gap-3 mb-3">
<!-- Completion Checkbox -->
<div class="flex items-center pt-1">
<Checkbox
:checked="isCompleted"
@update:checked="handleToggleComplete"
class="h-5 w-5"
/>
</div>
<!-- Event Details -->
<div class="flex-1 min-w-0">
<!-- Title and badges -->
<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': isCompleted }">
{{ event.title }}
</h3>
<Badge v-if="isAdminEvent" variant="secondary" class="shrink-0">
Admin
</Badge>
</div>
<!-- Date/Time -->
<div class="flex items-center gap-4 text-sm text-muted-foreground mb-2 flex-wrap">
<div class="flex items-center gap-1.5">
<Calendar class="h-4 w-4" />
<span>{{ formattedDate }}</span>
</div>
<div v-if="formattedTimeRange" class="flex items-center gap-1.5">
<Clock class="h-4 w-4" />
<span>{{ formattedTimeRange }}</span>
</div>
</div>
<!-- Location -->
<div v-if="event.location" class="flex items-center gap-1.5 text-sm text-muted-foreground mb-3">
<MapPin class="h-4 w-4" />
<span>{{ event.location }}</span>
</div>
<!-- Description/Content -->
<div v-if="event.description || event.content" class="text-sm mb-3">
<p class="whitespace-pre-wrap break-words">{{ event.description || event.content }}</p>
</div>
<!-- Completion info -->
<div v-if="isCompleted && completion" class="text-xs text-muted-foreground">
Completed by {{ getDisplayName(completion.pubkey) }}
<span v-if="completion.notes"> - {{ completion.notes }}</span>
</div>
<!-- Author (if not admin) -->
<div v-if="!isAdminEvent" class="text-xs text-muted-foreground mt-2">
Posted by {{ getDisplayName(event.pubkey) }}
</div>
</div>
</div>
</div>
</template>