Add client-side watch/bookmark functionality: users can save events to localStorage without RSVPing via a bookmark button next to the "I'm attending" CTA. Watched events appear in the event list with a "Watching" label. Bookmark is only visible for visitors (not attendees or organizers). Includes spec, plan, research, tasks, unit tests, and E2E tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
112 lines
3.2 KiB
TypeScript
112 lines
3.2 KiB
TypeScript
export interface StoredEvent {
|
|
eventToken: string
|
|
organizerToken?: string
|
|
title: string
|
|
dateTime: string
|
|
rsvpToken?: string
|
|
rsvpName?: string
|
|
}
|
|
|
|
import { ref } from 'vue'
|
|
|
|
const STORAGE_KEY = 'fete:events'
|
|
|
|
const version = ref(0)
|
|
|
|
export function isValidStoredEvent(e: unknown): e is StoredEvent {
|
|
if (typeof e !== 'object' || e === null) return false
|
|
const obj = e as Record<string, unknown>
|
|
return (
|
|
typeof obj.eventToken === 'string' &&
|
|
obj.eventToken.length > 0 &&
|
|
typeof obj.title === 'string' &&
|
|
obj.title.length > 0 &&
|
|
typeof obj.dateTime === 'string' &&
|
|
obj.dateTime.length > 0 &&
|
|
!isNaN(new Date(obj.dateTime).getTime())
|
|
)
|
|
}
|
|
|
|
function readEvents(): StoredEvent[] {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY)
|
|
return raw ? (JSON.parse(raw) as StoredEvent[]) : []
|
|
} catch {
|
|
return []
|
|
}
|
|
}
|
|
|
|
function writeEvents(events: StoredEvent[]): void {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(events))
|
|
version.value++
|
|
}
|
|
|
|
export function useEventStorage() {
|
|
function saveCreatedEvent(event: StoredEvent): void {
|
|
const events = readEvents().filter((e) => e.eventToken !== event.eventToken)
|
|
events.push(event)
|
|
writeEvents(events)
|
|
}
|
|
|
|
function getStoredEvents(): StoredEvent[] {
|
|
void version.value
|
|
return readEvents()
|
|
}
|
|
|
|
function getOrganizerToken(eventToken: string): string | undefined {
|
|
const event = readEvents().find((e) => e.eventToken === eventToken)
|
|
return event?.organizerToken
|
|
}
|
|
|
|
function saveRsvp(eventToken: string, rsvpToken: string, rsvpName: string, title: string, dateTime: string): void {
|
|
const events = readEvents()
|
|
const existing = events.find((e) => e.eventToken === eventToken)
|
|
if (existing) {
|
|
existing.rsvpToken = rsvpToken
|
|
existing.rsvpName = rsvpName
|
|
} else {
|
|
events.push({ eventToken, title, dateTime, rsvpToken, rsvpName })
|
|
}
|
|
writeEvents(events)
|
|
}
|
|
|
|
function getRsvp(eventToken: string): { rsvpToken: string; rsvpName: string } | undefined {
|
|
const event = readEvents().find((e) => e.eventToken === eventToken)
|
|
if (event?.rsvpToken && event?.rsvpName) {
|
|
return { rsvpToken: event.rsvpToken, rsvpName: event.rsvpName }
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function removeRsvp(eventToken: string): void {
|
|
const events = readEvents()
|
|
const event = events.find((e) => e.eventToken === eventToken)
|
|
if (event) {
|
|
delete event.rsvpToken
|
|
delete event.rsvpName
|
|
writeEvents(events)
|
|
}
|
|
}
|
|
|
|
function saveWatch(eventToken: string, title: string, dateTime: string): void {
|
|
const events = readEvents()
|
|
const existing = events.find((e) => e.eventToken === eventToken)
|
|
if (!existing) {
|
|
events.push({ eventToken, title, dateTime })
|
|
writeEvents(events)
|
|
}
|
|
}
|
|
|
|
function isStored(eventToken: string): boolean {
|
|
void version.value
|
|
return readEvents().some((e) => e.eventToken === eventToken)
|
|
}
|
|
|
|
function removeEvent(eventToken: string): void {
|
|
const events = readEvents().filter((e) => e.eventToken !== eventToken)
|
|
writeEvents(events)
|
|
}
|
|
|
|
return { saveCreatedEvent, getStoredEvents, getOrganizerToken, saveRsvp, getRsvp, removeRsvp, saveWatch, isStored, removeEvent }
|
|
}
|