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

5.5 KiB

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:

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