# Research: View Event Landing Page (007) **Date**: 2026-03-06 | **Status**: Complete ## R-1: Timezone Field (Cross-Cutting) **Decision**: Add `timezone` String field (IANA zone ID) to Event entity, JPA entity, and OpenAPI schemas (both Create and Get). **Rationale**: The spec requires displaying the IANA timezone name (e.g. "Europe/Berlin") alongside the event time. `OffsetDateTime` preserves the offset (e.g. `+01:00`) but loses the IANA zone name. Since Europe/Berlin and Africa/Lagos both use `+01:00`, the zone name must be stored separately. **Alternatives considered**: - Store `ZonedDateTime` instead of `OffsetDateTime` — rejected because `OffsetDateTime` is already the established type in the stack (see `datetime-best-practices.md`), and `ZonedDateTime` serialization is non-standard in JSON/OpenAPI. - Derive timezone from offset — rejected because offset-to-zone mapping is ambiguous. **Impact on US-1 (Create Event)**: - `CreateEventRequest` gains a required `timezone` field (string, IANA zone ID). - `CreateEventResponse` gains a `timezone` field. - Frontend auto-detects via `Intl.DateTimeFormat().resolvedOptions().timeZone`. - Backend validates against `java.time.ZoneId.getAvailableZoneIds()`. - JPA: new `VARCHAR(64)` column `timezone` on `events` table. - Liquibase changeset: add `timezone` column. Existing events without timezone get `UTC` as default (pre-launch, destructive migration acceptable). ## R-2: GET Endpoint Design **Decision**: `GET /api/events/{token}` returns public event data. Uses the existing hexagonal architecture pattern. **Rationale**: Follows the established pattern from `POST /events`. The event token is the public identifier — no auth required. **Flow**: 1. `EventController` implements generated `EventsApi.getEvent()`. 2. New inbound port: `GetEventUseCase` with `getByEventToken(UUID): Optional`. 3. `EventService` implements the use case, delegates to `EventRepository.findByEventToken()` (already exists). 4. Controller maps domain `Event` to `GetEventResponse` DTO. 5. 404 returns `ProblemDetail` (RFC 9457) — no event data leaked. **Alternatives considered**: - Separate `/event/{token}` path (singular) — rejected because OpenAPI groups by resource; `/events/{token}` is RESTful convention. - Note: Frontend route is `/event/:token` (spec clarification), but API path is `/api/events/{token}`. These are independent. ## R-3: Attendee Count Without RSVP Model **Decision**: Include `attendeeCount` (integer) in the `GetEventResponse`. Return `0` until the RSVP feature (US-8+) is implemented. **Rationale**: FR-001 requires attendee count display. The API contract should be stable from the start — consumers should not need to change when RSVP is added later. Returning `0` is correct (no RSVPs exist yet). **Future hook**: When RSVP is implemented, `EventService` or a dedicated query will `COUNT(*) WHERE event_id = ? AND status = 'ATTENDING'`. **Alternatives considered**: - Omit `attendeeCount` until RSVP exists — rejected because it would require API consumers to handle the field's absence, then handle its presence later. Breaking change. - Add a stub RSVP table now — rejected (YAGNI, violates Principle IV). ## R-4: Expired Event Detection **Decision**: Server-side. The `GetEventResponse` includes a boolean `expired` field, computed by comparing `expiryDate` with the server's current date. **Rationale**: Server is the source of truth for time. Client clocks may be wrong. The frontend uses this flag to toggle the "event has ended" state. **Computation**: `event.getExpiryDate().isBefore(LocalDate.now(clock))` — uses the injected `Clock` bean (already exists for testability in `EventService`). **Alternatives considered**: - Client-side comparison — rejected because client clock may differ from server, leading to inconsistent behavior. - Separate endpoint for status — rejected (over-engineering). ## R-5: URL Pattern **Decision**: Frontend route stays at `/events/:token` (plural). API path is `/api/events/{token}`. Both use the plural RESTful convention consistently. **Rationale**: `/events/:token` is the standard REST resource pattern (collection + identifier). The existing router already uses this path. Consistency between frontend route and API resource name reduces cognitive overhead. **Impact**: No route change needed — the existing `/events/:token` route in the router is correct. ## R-6: Skeleton Shimmer Loading State **Decision**: CSS-only shimmer animation using a gradient sweep. No additional dependencies. **Rationale**: The spec requires skeleton-shimmer placeholders during API loading. A CSS-only approach is lightweight and matches the dependency discipline principle. **Implementation pattern**: ```css .skeleton { background: linear-gradient(90deg, var(--color-card) 25%, #e0e0e0 50%, var(--color-card) 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; border-radius: var(--radius-card); } @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } } ``` Skeleton blocks match the approximate shape/size of the real content fields (title, date, location, etc.). ## R-7: Cancelled Event State (Deferred) **Decision**: The `GetEventResponse` does NOT include cancellation fields yet. US-3 (view cancelled event) is explicitly deferred until US-18 (cancel event) is implemented. **Rationale**: Spec says "[Deferred until US-18 is implemented]". Adding unused fields violates Principle IV (KISS). **Future hook**: When US-18 lands, add `cancelled: boolean` and `cancellationMessage: string` to the response schema.