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.
237 lines
7.5 KiB
Vue
237 lines
7.5 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref } from 'vue'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Button } from '@/components/ui/button'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
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
|
|
adminPubkeys?: string[]
|
|
}
|
|
|
|
interface Emits {
|
|
(e: 'toggle-complete', event: ScheduledEvent): void
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
adminPubkeys: () => []
|
|
})
|
|
|
|
const emit = defineEmits<Emits>()
|
|
|
|
// Confirmation dialog state
|
|
const showConfirmDialog = ref(false)
|
|
|
|
// Event address for tracking completion
|
|
const eventAddress = computed(() => `31922:${props.event.pubkey}:${props.event.dTag}`)
|
|
|
|
// 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 completable (task type)
|
|
const isCompletable = computed(() => props.event.eventType === 'task')
|
|
|
|
// 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 mark complete button click - show confirmation dialog
|
|
function handleMarkComplete() {
|
|
console.log('🔘 Mark Complete button clicked for event:', props.event.title)
|
|
showConfirmDialog.value = true
|
|
}
|
|
|
|
// Confirm and execute mark complete
|
|
function confirmMarkComplete() {
|
|
console.log('✅ Confirmed mark complete for event:', props.event.title)
|
|
emit('toggle-complete', props.event)
|
|
showConfirmDialog.value = false
|
|
}
|
|
|
|
// Cancel mark complete
|
|
function cancelMarkComplete() {
|
|
showConfirmDialog.value = false
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Collapsible class="border-b md:border md:rounded-lg bg-card transition-all"
|
|
:class="{ 'opacity-60': isCompletable && isCompleted }">
|
|
<!-- Collapsed View (Trigger) -->
|
|
<CollapsibleTrigger as-child>
|
|
<div class="flex items-center gap-3 p-3 md:p-4 cursor-pointer hover:bg-accent/50 transition-colors">
|
|
<!-- Time -->
|
|
<div class="flex items-center gap-1.5 text-sm text-muted-foreground shrink-0">
|
|
<Clock class="h-3.5 w-3.5" />
|
|
<span class="font-medium">{{ formattedTimeRange || formattedDate }}</span>
|
|
</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 -->
|
|
<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>
|
|
</CollapsibleTrigger>
|
|
|
|
<!-- Expanded View (Content) -->
|
|
<CollapsibleContent class="p-4 md:p-6 pt-0">
|
|
<!-- Event Details -->
|
|
<div class="flex-1 min-w-0">
|
|
<!-- 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 (only for completable events) -->
|
|
<div v-if="isCompletable && isCompleted && getCompletion(eventAddress)" class="text-xs text-muted-foreground mb-3">
|
|
✓ Completed by {{ getDisplayName(getCompletion(eventAddress)!.pubkey) }}
|
|
<span v-if="getCompletion(eventAddress)!.notes"> - {{ getCompletion(eventAddress)!.notes }}</span>
|
|
</div>
|
|
|
|
<!-- Author (if not admin) -->
|
|
<div v-if="!isAdminEvent" class="text-xs text-muted-foreground mb-3">
|
|
Posted by {{ getDisplayName(event.pubkey) }}
|
|
</div>
|
|
|
|
<!-- Mark Complete Button (only for completable task events) -->
|
|
<div v-if="isCompletable && !isCompleted" class="mt-3">
|
|
<Button
|
|
@click.stop="handleMarkComplete"
|
|
variant="outline"
|
|
size="sm"
|
|
class="gap-2"
|
|
>
|
|
<CheckCircle class="h-4 w-4" />
|
|
Mark Complete
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</CollapsibleContent>
|
|
|
|
</Collapsible>
|
|
|
|
<!-- Confirmation Dialog -->
|
|
<Dialog :open="showConfirmDialog" @update:open="(val: boolean) => showConfirmDialog = val">
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Mark Event as Complete?</DialogTitle>
|
|
<DialogDescription>
|
|
This will mark "{{ event.title }}" as completed by you. Other users will be able to see that you completed this event.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter>
|
|
<Button variant="outline" @click="cancelMarkComplete">Cancel</Button>
|
|
<Button @click="confirmMarkComplete">Mark Complete</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</template>
|