diff --git a/frontend/src/composables/__tests__/useEventStorage.spec.ts b/frontend/src/composables/__tests__/useEventStorage.spec.ts new file mode 100644 index 0000000..98518a2 --- /dev/null +++ b/frontend/src/composables/__tests__/useEventStorage.spec.ts @@ -0,0 +1,119 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { useEventStorage } from '../useEventStorage' + +// jsdom provides a working localStorage in the window object +// but Node's --localstorage-file warning can be ignored +function clearStorage() { + try { + window.localStorage.setItem('fete:events', '[]') + } catch { + // Provide a minimal mock if localStorage is broken + const store: Record = {} + Object.defineProperty(globalThis, 'localStorage', { + value: { + getItem: (key: string) => store[key] ?? null, + setItem: (key: string, val: string) => { + store[key] = val + }, + removeItem: (key: string) => { + delete store[key] + }, + }, + writable: true, + configurable: true, + }) + } +} + +describe('useEventStorage', () => { + beforeEach(() => { + clearStorage() + }) + + it('returns empty array when no events stored', () => { + const { getStoredEvents } = useEventStorage() + expect(getStoredEvents()).toEqual([]) + }) + + it('saves and retrieves a created event', () => { + const { saveCreatedEvent, getStoredEvents } = useEventStorage() + + saveCreatedEvent({ + eventToken: 'abc-123', + organizerToken: 'org-456', + title: 'Birthday', + dateTime: '2026-06-15T20:00:00+02:00', + expiryDate: '2026-07-15', + }) + + const events = getStoredEvents() + expect(events).toHaveLength(1) + expect(events[0]!.eventToken).toBe('abc-123') + expect(events[0]!.organizerToken).toBe('org-456') + expect(events[0]!.title).toBe('Birthday') + }) + + it('returns organizer token for known event', () => { + const { saveCreatedEvent, getOrganizerToken } = useEventStorage() + + saveCreatedEvent({ + eventToken: 'abc-123', + organizerToken: 'org-456', + title: 'Test', + dateTime: '2026-06-15T20:00:00+02:00', + expiryDate: '2026-07-15', + }) + + expect(getOrganizerToken('abc-123')).toBe('org-456') + }) + + it('returns undefined organizer token for unknown event', () => { + const { getOrganizerToken } = useEventStorage() + expect(getOrganizerToken('unknown')).toBeUndefined() + }) + + it('stores multiple events independently', () => { + const { saveCreatedEvent, getStoredEvents } = useEventStorage() + + saveCreatedEvent({ + eventToken: 'event-1', + title: 'First', + dateTime: '2026-06-15T20:00:00+02:00', + expiryDate: '2026-07-15', + }) + + saveCreatedEvent({ + eventToken: 'event-2', + title: 'Second', + dateTime: '2026-07-15T20:00:00+02:00', + expiryDate: '2026-08-15', + }) + + const events = getStoredEvents() + expect(events).toHaveLength(2) + expect(events.map((e) => e.eventToken)).toContain('event-1') + expect(events.map((e) => e.eventToken)).toContain('event-2') + }) + + it('overwrites event with same token', () => { + const { saveCreatedEvent, getStoredEvents } = useEventStorage() + + saveCreatedEvent({ + eventToken: 'abc-123', + title: 'Old Title', + dateTime: '2026-06-15T20:00:00+02:00', + expiryDate: '2026-07-15', + }) + + saveCreatedEvent({ + eventToken: 'abc-123', + title: 'New Title', + dateTime: '2026-06-15T20:00:00+02:00', + expiryDate: '2026-07-15', + }) + + const events = getStoredEvents() + expect(events).toHaveLength(1) + expect(events[0]!.title).toBe('New Title') + }) +}) diff --git a/frontend/src/composables/useEventStorage.ts b/frontend/src/composables/useEventStorage.ts new file mode 100644 index 0000000..e5e062f --- /dev/null +++ b/frontend/src/composables/useEventStorage.ts @@ -0,0 +1,41 @@ +export interface StoredEvent { + eventToken: string + organizerToken?: string + title: string + dateTime: string + expiryDate: string +} + +const STORAGE_KEY = 'fete:events' + +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)) +} + +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[] { + return readEvents() + } + + function getOrganizerToken(eventToken: string): string | undefined { + const event = readEvents().find((e) => e.eventToken === eventToken) + return event?.organizerToken + } + + return { saveCreatedEvent, getStoredEvents, getOrganizerToken } +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 3e49915..07bc62d 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -10,12 +10,14 @@ const router = createRouter({ component: HomeView, }, { - path: '/about', - name: 'about', - // route level code-splitting - // this generates a separate chunk (About.[hash].js) for this route - // which is lazy-loaded when the route is visited. - component: () => import('../views/AboutView.vue'), + path: '/create', + name: 'create-event', + component: () => import('../views/EventCreateView.vue'), + }, + { + path: '/events/:token', + name: 'event', + component: () => import('../views/EventStubView.vue'), }, ], }) diff --git a/frontend/src/views/EventCreateView.vue b/frontend/src/views/EventCreateView.vue new file mode 100644 index 0000000..e48af62 --- /dev/null +++ b/frontend/src/views/EventCreateView.vue @@ -0,0 +1,258 @@ +