Files
fete/specs/007-view-event/research.md
nitrix 80d79c3596 Add design artifacts for view event feature (007)
Spec, research, data model, API contract, implementation plan, and
task breakdown for the public event detail page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 22:34:51 +01:00

101 lines
5.5 KiB
Markdown

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