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:
parent
875bf50765
commit
b15a8c21c0
8 changed files with 716 additions and 1 deletions
151
src/modules/nostr-feed/components/ScheduledEventCard.vue
Normal file
151
src/modules/nostr-feed/components/ScheduledEventCard.vue
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue