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>
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
ZonedDateTimeinstead ofOffsetDateTime— rejected becauseOffsetDateTimeis already the established type in the stack (seedatetime-best-practices.md), andZonedDateTimeserialization is non-standard in JSON/OpenAPI. - Derive timezone from offset — rejected because offset-to-zone mapping is ambiguous.
Impact on US-1 (Create Event):
CreateEventRequestgains a requiredtimezonefield (string, IANA zone ID).CreateEventResponsegains atimezonefield.- Frontend auto-detects via
Intl.DateTimeFormat().resolvedOptions().timeZone. - Backend validates against
java.time.ZoneId.getAvailableZoneIds(). - JPA: new
VARCHAR(64)columntimezoneoneventstable. - Liquibase changeset: add
timezonecolumn. Existing events without timezone getUTCas 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:
EventControllerimplements generatedEventsApi.getEvent().- New inbound port:
GetEventUseCasewithgetByEventToken(UUID): Optional<Event>. EventServiceimplements the use case, delegates toEventRepository.findByEventToken()(already exists).- Controller maps domain
EventtoGetEventResponseDTO. - 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
attendeeCountuntil 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.