# 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<Event>`.
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.
