{ "$schema": "https://shadcn-vue.com/schema/registry-item.json", "name": "event-calendar-data", "title": "Event Calendar — Data (client)", "description": "Optional client composable that loads events from and persists them to a REST API (/api/events), converting ISO<->Date. Pairs with the persistence layer. Depends on the core calendar's types.", "dependencies": [ "date-fns" ], "registryDependencies": [ "https://event-calendar.anorebel.net/r/event-calendar.json" ], "files": [ { "path": "app/registry/event-calendar-data/useCalendarData.ts", "content": "import { ref } from \"vue\"\nimport { parseISO } from \"date-fns\"\nimport type { CalendarEvent } from \"@/components/event-calendar/types\"\n\n// Wire-format event: dates are ISO strings over HTTP.\ninterface EventDTO extends Omit {\n startDate: string\n endDate: string\n}\n\ninterface PaginatedResponse {\n data: T[]\n total: number\n page: number\n limit: number\n hasMore: boolean\n}\n\n// A change broadcast over the WebSocket. Matches the server's EventChange.\nexport type RemoteEventChange =\n | { type: \"created\"; event: EventDTO }\n | { type: \"updated\"; event: EventDTO }\n | { type: \"deleted\"; id: string; ownerId?: string | null }\n\nconst dtoToEvent = (dto: EventDTO): CalendarEvent => ({\n ...dto,\n startDate: parseISO(dto.startDate),\n endDate: parseISO(dto.endDate),\n})\n\nconst eventToBody = (e: Partial): Record => {\n const body: Record = { ...e }\n if (e.startDate instanceof Date) body.startDate = e.startDate.toISOString()\n if (e.endDate instanceof Date) body.endDate = e.endDate.toISOString()\n return body\n}\n\n// Owns the calendar's data: loads events from /api/events and performs CRUD.\n// The calendar component stays controlled (events in via prop, mutations via\n// emits); this composable is the persistence layer app.vue delegates to.\nexport function useCalendarData() {\n const events = ref([])\n const isLoading = ref(false)\n const loadError = ref(null)\n\n // Ids this client just wrote, so real-time echoes of its own changes can be\n // ignored. Entries expire so the set can't grow unbounded.\n const recentWrites = new Map()\n const markWrite = (id: string) => recentWrites.set(id, Date.now())\n const isOwnEcho = (change: RemoteEventChange) => {\n const id = change.type === \"deleted\" ? change.id : change.event?.id\n if (!id) return false\n const t = recentWrites.get(id)\n if (t === undefined) return false\n if (Date.now() - t > 5000) {\n recentWrites.delete(id)\n return false\n }\n return true\n }\n\n async function load() {\n isLoading.value = true\n loadError.value = null\n try {\n // Fetch a broad window so month/week/day views all have data.\n const res = await $fetch>(\"/api/events\", {\n query: { limit: 500 },\n })\n events.value = res.data.map(dtoToEvent)\n } catch (err) {\n loadError.value = err instanceof Error ? err.message : \"Failed to load events\"\n throw err\n } finally {\n isLoading.value = false\n }\n }\n\n async function createEvent(event: CalendarEvent): Promise {\n const dto = await $fetch(\"/api/events\", {\n method: \"POST\",\n body: eventToBody(event),\n })\n const created = dtoToEvent(dto)\n markWrite(created.id)\n events.value = [...events.value, created]\n return created\n }\n\n async function updateEvent(event: CalendarEvent): Promise {\n const dto = await $fetch(`/api/events/${event.id}`, {\n method: \"PATCH\",\n body: eventToBody(event),\n })\n const updated = dtoToEvent(dto)\n markWrite(updated.id)\n events.value = events.value.map(e => (e.id === updated.id ? updated : e))\n return updated\n }\n\n async function deleteEvent(id: string): Promise {\n markWrite(id)\n await $fetch(`/api/events/${id}`, { method: \"DELETE\" })\n events.value = events.value.filter(e => e.id !== id)\n }\n\n // Apply a change that originated elsewhere (a real-time broadcast, section 8),\n // idempotently — used so remote edits appear without a reload.\n function applyRemote(change: RemoteEventChange) {\n if (change.type === \"deleted\") {\n events.value = events.value.filter(e => e.id !== change.id)\n return\n }\n const incoming = dtoToEvent(change.event)\n const idx = events.value.findIndex(e => e.id === incoming.id)\n if (idx === -1) events.value = [...events.value, incoming]\n else events.value = events.value.map(e => (e.id === incoming.id ? incoming : e))\n }\n\n return { events, isLoading, loadError, load, createEvent, updateEvent, deleteEvent, applyRemote, isOwnEcho }\n}\n", "type": "registry:hook", "target": "~/composables/useCalendarData.ts" } ], "type": "registry:hook" }