From 80d79c35969c644735de631e67f34664246e4c94 Mon Sep 17 00:00:00 2001 From: nitrix Date: Fri, 6 Mar 2026 22:33:04 +0100 Subject: [PATCH] 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 --- .specify/memory/constitution.md | 6 +- .specify/memory/ideen.md | 1 + CLAUDE.md | 7 + specs/007-view-event/contracts/get-event.yaml | 94 ++++++++ specs/007-view-event/data-model.md | 56 +++++ specs/007-view-event/plan.md | 89 +++++++ specs/007-view-event/quickstart.md | 39 +++ specs/007-view-event/research.md | 100 ++++++++ specs/007-view-event/spec.md | 32 ++- specs/007-view-event/tasks.md | 225 ++++++++++++++++++ 10 files changed, 638 insertions(+), 11 deletions(-) create mode 100644 specs/007-view-event/contracts/get-event.yaml create mode 100644 specs/007-view-event/data-model.md create mode 100644 specs/007-view-event/plan.md create mode 100644 specs/007-view-event/quickstart.md create mode 100644 specs/007-view-event/research.md create mode 100644 specs/007-view-event/tasks.md diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index 43cb8d0..cd8cf67 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -107,8 +107,10 @@ Accessibility is a baseline requirement, not an afterthought. rationale. Never rewrite or delete the original decision. - The visual design system in `.specify/memory/design-system.md` is authoritative. All frontend implementation MUST follow it. -- Research reports go to `docs/agents/research/`, implementation plans to - `docs/agents/plan/`. +- Feature specs, research, and plans live in `specs/NNN-feature-name/` + (spec-kit format). Cross-cutting research goes to + `.specify/memory/research/`, cross-cutting plans to + `.specify/memory/plans/`. - Conversation and brainstorming in German; code, comments, commits, and documentation in English. - Documentation lives in the README. No wiki, no elaborate docs site. diff --git a/.specify/memory/ideen.md b/.specify/memory/ideen.md index 809e0f9..15217e4 100644 --- a/.specify/memory/ideen.md +++ b/.specify/memory/ideen.md @@ -33,6 +33,7 @@ Person erstellt via App eine Veranstaltung und schickt seine Freunden irgendwie * Updaten der Veranstaltung * Einsicht angemeldete Gäste, kann bei Bedarf Einträge entfernen * Featureideen: + * Link-Previews (OpenGraph Meta-Tags): Generische OG-Tags mit App-Branding (z.B. "fete — Du wurdest eingeladen") damit geteilte Links in WhatsApp/Signal/Telegram hübsch aussehen. Keine Event-Daten an Crawler aus Privacy-Gründen. → Eigene User Story. * Kalender-Integration: .ics-Download + optional webcal:// für Live-Updates bei Änderungen * Änderungen zum ursprünglichen Inhalt (z.b. geändertes datum/ort) werden iwi hervorgehoben * Veranstalter kann Updatenachrichten im Event posten, pro Device wird via LocalStorage gemerkt was man schon gesehen hat (Badge/Hervorhebung für neue Updates) diff --git a/CLAUDE.md b/CLAUDE.md index 1737c2a..ffc6b4a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -49,3 +49,10 @@ The following skills are available and should be used for their respective purpo - The loop runner is `ralph.sh`. Each run lives in its own directory under `.ralph/`. - Run directories contain: `instructions.md` (prompt), `chief-wiggum.md` (directives), `answers.md` (human answers), `questions.md` (Ralph's questions), `progress.txt` (iteration log), `meta.md` (metadata), `run.log` (execution log). - Project specifications (user stories, setup tasks, personas, etc.) live in `specs/` (feature dirs) and `.specify/memory/` (cross-cutting docs). + +## Active Technologies +- Java 25 (backend), TypeScript 5.9 (frontend) + Spring Boot 3.5.x, Vue 3, Vue Router 5, openapi-fetch, openapi-typescript (007-view-event) +- PostgreSQL (JPA via Spring Data, Liquibase migrations) (007-view-event) + +## Recent Changes +- 007-view-event: Added Java 25 (backend), TypeScript 5.9 (frontend) + Spring Boot 3.5.x, Vue 3, Vue Router 5, openapi-fetch, openapi-typescript diff --git a/specs/007-view-event/contracts/get-event.yaml b/specs/007-view-event/contracts/get-event.yaml new file mode 100644 index 0000000..7b1338c --- /dev/null +++ b/specs/007-view-event/contracts/get-event.yaml @@ -0,0 +1,94 @@ +# OpenAPI contract addition for GET /events/{token} +# To be merged into backend/src/main/resources/openapi/api.yaml + +paths: + /events/{token}: + get: + operationId: getEvent + summary: Get public event details by token + tags: + - events + parameters: + - name: token + in: path + required: true + schema: + type: string + format: uuid + description: Public event token + responses: + "200": + description: Event found + content: + application/json: + schema: + $ref: "#/components/schemas/GetEventResponse" + "404": + description: Event not found + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ProblemDetail" + +components: + schemas: + GetEventResponse: + type: object + required: + - eventToken + - title + - dateTime + - timezone + - attendeeCount + - expired + properties: + eventToken: + type: string + format: uuid + description: Public event token + example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" + title: + type: string + description: Event title + example: "Summer BBQ" + description: + type: string + description: Event description (absent if not set) + example: "Bring your own drinks!" + dateTime: + type: string + format: date-time + description: Event date/time with organizer's UTC offset + example: "2026-03-15T20:00:00+01:00" + timezone: + type: string + description: IANA timezone name of the organizer + example: "Europe/Berlin" + location: + type: string + description: Event location (absent if not set) + example: "Central Park, NYC" + attendeeCount: + type: integer + minimum: 0 + description: Number of confirmed attendees (attending=true) + example: 12 + expired: + type: boolean + description: Whether the event's expiry date has passed + example: false + + # Modification to existing CreateEventRequest — add timezone field + # CreateEventRequest (additions): + # timezone: + # type: string + # description: IANA timezone of the organizer + # example: "Europe/Berlin" + # (make required) + + # Modification to existing CreateEventResponse — add timezone field + # CreateEventResponse (additions): + # timezone: + # type: string + # description: IANA timezone of the organizer + # example: "Europe/Berlin" diff --git a/specs/007-view-event/data-model.md b/specs/007-view-event/data-model.md new file mode 100644 index 0000000..d213490 --- /dev/null +++ b/specs/007-view-event/data-model.md @@ -0,0 +1,56 @@ +# Data Model: View Event Landing Page (007) + +**Date**: 2026-03-06 + +## Entities + +### Event (modified — adds `timezone` field) + +| Field | Type | Required | Constraints | Notes | +|-----------------|------------------|----------|--------------------------|----------------------------------| +| id | Long | yes | BIGSERIAL, PK | Internal only, never exposed | +| eventToken | UUID | yes | UNIQUE, NOT NULL | Public identifier in URLs | +| organizerToken | UUID | yes | UNIQUE, NOT NULL | Secret, never in public API | +| title | String | yes | 1–200 chars | | +| description | String | no | max 2000 chars | | +| dateTime | OffsetDateTime | yes | | Organizer's original offset | +| timezone | String | yes | IANA zone ID, max 64 | **NEW** — e.g. "Europe/Berlin" | +| location | String | no | max 500 chars | | +| expiryDate | LocalDate | yes | Must be future at create | Auto-deletion trigger | +| createdAt | OffsetDateTime | yes | Server-generated | | + +**Validation rules**: +- `timezone` must be a valid IANA zone ID (`ZoneId.getAvailableZoneIds()`). +- `expiryDate` must be in the future at creation time (existing rule). + +**State transitions**: +- Active → Expired: when `expiryDate < today` (computed, not stored). +- Active → Cancelled: future (US-18), adds `cancelledAt` + `cancellationMessage`. + +### RSVP (future — not created in this feature) + +Documented here for context only. Created when the RSVP feature (US-8+) is implemented. + +| Field | Type | Required | Constraints | +|------------|---------|----------|------------------------------| +| id | Long | yes | BIGSERIAL, PK | +| eventId | Long | yes | FK → events.id | +| guestName | String | yes | 1–100 chars | +| attending | Boolean | yes | true = attending | +| createdAt | OffsetDateTime | yes | Server-generated | + +## Relationships + +``` +Event 1 ←── * RSVP (future) +``` + +## Type Mapping (full stack) + +| Concept | Java | PostgreSQL | OpenAPI | TypeScript | +|--------------|-------------------|---------------|---------------------|------------| +| Event time | `OffsetDateTime` | `timestamptz` | `string` `date-time`| `string` | +| Timezone | `String` | `varchar(64)` | `string` | `string` | +| Expiry date | `LocalDate` | `date` | `string` `date` | `string` | +| Token | `UUID` | `uuid` | `string` `uuid` | `string` | +| Count | `int` | `integer` | `integer` | `number` | diff --git a/specs/007-view-event/plan.md b/specs/007-view-event/plan.md new file mode 100644 index 0000000..7d8d4ac --- /dev/null +++ b/specs/007-view-event/plan.md @@ -0,0 +1,89 @@ +# Implementation Plan: View Event Landing Page + +**Branch**: `007-view-event` | **Date**: 2026-03-06 | **Spec**: [spec.md](spec.md) +**Input**: Feature specification from `/specs/007-view-event/spec.md` + +## Summary + +Add a public event detail page at `/events/:token` that displays event information (title, date/time with IANA timezone, description, location, attendee count) without requiring authentication. The page handles four states: loaded, expired ("event has ended"), not found (404), and server error (retry button). Loading uses skeleton-shimmer placeholders. Backend adds `GET /events/{token}` endpoint and a `timezone` field to the Event model (cross-cutting change to US-1). + +## Technical Context + +**Language/Version**: Java 25 (backend), TypeScript 5.9 (frontend) +**Primary Dependencies**: Spring Boot 3.5.x, Vue 3, Vue Router 5, openapi-fetch, openapi-typescript +**Storage**: PostgreSQL (JPA via Spring Data, Liquibase migrations) +**Testing**: JUnit (backend), Vitest (frontend unit), Playwright + MSW (frontend E2E) +**Target Platform**: Self-hosted web application (Docker) +**Project Type**: Web service + SPA +**Performance Goals**: N/A (single-user scale, self-hosted) +**Constraints**: No external resources (CDNs, fonts, tracking), WCAG AA, privacy-first +**Scale/Scope**: Single new view + one new API endpoint + one cross-cutting model change + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +| Principle | Status | Notes | +|-----------|--------|-------| +| I. Privacy by Design | PASS | No PII exposed. Only attendee count shown (not names). No external resources. No tracking. | +| II. Test-Driven Methodology | PASS | TDD enforced: backend unit tests, frontend unit tests, E2E tests per spec. | +| III. API-First Development | PASS | OpenAPI spec updated first. Types generated. Response schemas include `example:` fields. | +| IV. Simplicity & Quality | PASS | Minimal changes: one GET endpoint, one new view, one model field. `attendeeCount` returns 0 (no RSVP stub). Cancelled state deferred. | +| V. Dependency Discipline | PASS | No new dependencies. Skeleton shimmer is CSS-only. | +| VI. Accessibility | PASS | Semantic HTML, ARIA attributes, keyboard navigable, WCAG AA contrast via design system. | + +**Post-Phase-1 re-check**: All gates still pass. The `timezone` field addition is a justified cross-cutting change documented in research.md R-1. + +## Project Structure + +### Documentation (this feature) + +```text +specs/007-view-event/ +├── plan.md # This file +├── spec.md # Feature specification +├── research.md # Phase 0: research decisions +├── data-model.md # Phase 1: entity definitions +├── quickstart.md # Phase 1: implementation overview +├── contracts/ +│ └── get-event.yaml # Phase 1: GET endpoint contract +└── tasks.md # Phase 2: implementation tasks (via /speckit.tasks) +``` + +### Source Code (repository root) + +```text +backend/ +├── src/main/java/de/fete/ +│ ├── domain/ +│ │ ├── model/Event.java # Add timezone field +│ │ └── port/in/GetEventUseCase.java # NEW: inbound port +│ ├── application/service/EventService.java # Implement GetEventUseCase +│ ├── adapter/ +│ │ ├── in/web/EventController.java # Implement getEvent() +│ │ └── out/persistence/ +│ │ ├── EventJpaEntity.java # Add timezone column +│ │ └── EventPersistenceAdapter.java # Map timezone field +│ └── config/ +├── src/main/resources/ +│ ├── openapi/api.yaml # Add GET endpoint + timezone +│ └── db/changelog/ # Liquibase: add timezone column +└── src/test/java/de/fete/ # Unit + integration tests + +frontend/ +├── src/ +│ ├── api/schema.d.ts # Regenerated from OpenAPI +│ ├── views/EventDetailView.vue # NEW: event detail page +│ ├── views/EventCreateView.vue # Add timezone to create request +│ ├── router/index.ts # Point /events/:token to EventDetailView +│ └── assets/main.css # Skeleton shimmer styles +├── e2e/ +│ └── event-view.spec.ts # NEW: E2E tests for view event +└── src/__tests__/ # Unit tests for EventDetailView +``` + +**Structure Decision**: Existing web application structure (backend + frontend). No new packages or modules — extends existing hexagonal architecture with one new inbound port and one new frontend view. + +## Complexity Tracking + +No constitution violations. No entries needed. diff --git a/specs/007-view-event/quickstart.md b/specs/007-view-event/quickstart.md new file mode 100644 index 0000000..b9c27ab --- /dev/null +++ b/specs/007-view-event/quickstart.md @@ -0,0 +1,39 @@ +# Quickstart: View Event Landing Page (007) + +## What this feature does + +Adds a public event detail page at `/events/:token`. Guests open a shared link and see: +- Event title, date/time (with IANA timezone), description, location +- Count of confirmed attendees (no names) +- "Event has ended" state for expired events +- "Event not found" for invalid tokens +- Skeleton shimmer while loading + +## Prerequisites + +- US-1 (Create Event) is implemented — Event entity, JPA persistence, POST endpoint exist. +- No RSVP model yet — attendee count returns 0 until RSVP feature is built. + +## Key changes + +### Backend + +1. **OpenAPI**: Add `GET /events/{token}` endpoint + `GetEventResponse` schema. Add `timezone` field to `CreateEventRequest`, `CreateEventResponse`, and `GetEventResponse`. +2. **Domain**: Add `timezone` (String) to `Event.java`. +3. **Persistence**: Add `timezone` column to `EventJpaEntity`, Liquibase migration. +4. **Use case**: New `GetEventUseCase` (inbound port) + implementation in `EventService`. +5. **Controller**: `EventController` implements `getEvent()` — maps to `GetEventResponse`, computes `expired` and `attendeeCount`. + +### Frontend + +1. **API types**: Regenerate `schema.d.ts` from updated OpenAPI spec. +2. **EventDetailView.vue**: New view component — fetches event by token, renders detail card. +3. **Router**: Replace `EventStubView` import at `/events/:token` with `EventDetailView`. +4. **States**: Loading (skeleton shimmer), loaded, expired, not-found, server-error (retry button). +5. **Create form**: Send `timezone` field (auto-detected via `Intl.DateTimeFormat`). + +### Testing + +- Backend: Unit tests for `GetEventUseCase`, controller tests for GET endpoint (200, 404). +- Frontend: Unit tests for EventDetailView (all states). +- E2E: Playwright tests with MSW mocks for all states (loaded, expired, not-found, error). diff --git a/specs/007-view-event/research.md b/specs/007-view-event/research.md new file mode 100644 index 0000000..3575efb --- /dev/null +++ b/specs/007-view-event/research.md @@ -0,0 +1,100 @@ +# 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. diff --git a/specs/007-view-event/spec.md b/specs/007-view-event/spec.md index 7c0f8e6..812f711 100644 --- a/specs/007-view-event/spec.md +++ b/specs/007-view-event/spec.md @@ -9,18 +9,18 @@ ### User Story 1 - View event details as guest (Priority: P1) -A guest receives a shared event link, opens it, and sees all relevant event information: title, description (if provided), date and time, location (if provided), and the list of confirmed attendees with a count. +A guest receives a shared event link, opens it, and sees all relevant event information: title, description (if provided), date and time, location (if provided), and the count of confirmed attendees. **Why this priority**: Core value of the feature — without this, no other part of the event page is meaningful. -**Independent Test**: Can be fully tested by navigating to a valid event URL and verifying all event fields are displayed correctly, including attendee list and count. +**Independent Test**: Can be fully tested by navigating to a valid event URL and verifying all event fields are displayed correctly, including attendee count. **Acceptance Scenarios**: 1. **Given** a valid event link, **When** a guest opens the URL, **Then** the page displays the event title, date and time, and attendee count. 2. **Given** a valid event link for an event with optional fields set, **When** a guest opens the URL, **Then** the description and location are also displayed. 3. **Given** a valid event link for an event with optional fields absent, **When** a guest opens the URL, **Then** only the required fields are shown — no placeholder text for missing optional fields. -4. **Given** a valid event with RSVPs, **When** a guest opens the event page, **Then** the names of all confirmed attendees ("attending") are listed and a total count is shown. +4. **Given** a valid event with RSVPs, **When** a guest opens the event page, **Then** only the total count of confirmed attendees is shown — individual names are NOT displayed to guests (names are only visible to the organizer via the organizer view). 5. **Given** an event page, **When** it is rendered, **Then** no external resources (CDNs, fonts, tracking scripts) are loaded — all assets are served from the app's own domain. 6. **Given** a guest with no account, **When** they open the event URL, **Then** the page loads without any login, account, or access code required. @@ -70,9 +70,9 @@ A guest navigates to an event URL that no longer resolves — the event was dele ### Edge Cases -- What happens when the event has no attendees yet? — Attendee list is empty; count shows 0. +- What happens when the event has no attendees yet? — Count shows 0. - What happens when the event has been cancelled after US-18 is implemented? — Renders cancelled state with optional message; RSVP hidden. [Deferred] -- What happens when the server is temporarily unavailable? — [NEEDS EXPANSION] +- What happens when the server is temporarily unavailable? — The page displays a generic, friendly error message with a manual "Retry" button. No automatic retry. - How does the page behave when JavaScript is disabled? — Per Q-3 resolution: the app is a SPA; JavaScript-dependent rendering is acceptable. ## Requirements @@ -81,7 +81,7 @@ A guest navigates to an event URL that no longer resolves — the event was dele - **FR-001**: The event page MUST display: title, date and time, and attendee count for any valid event. - **FR-002**: The event page MUST display description and location when those optional fields are set on the event. -- **FR-003**: The event page MUST list the names of all confirmed attendees (those who RSVPed "attending"). +- **FR-003**: The public event page MUST display only the count of confirmed attendees. Individual attendee names MUST NOT be shown to guests — names are only visible to the organizer (organizer view, separate user story). - **FR-004**: If the event's expiry date has passed, the page MUST render a clear "this event has ended" state and MUST NOT show any RSVP actions. - **FR-005**: If the event has been cancelled (US-18), the page MUST display a "cancelled" state with the cancellation message (if provided) and MUST NOT show any RSVP actions. [Deferred until US-18 is implemented] - **FR-006**: If the event token does not match any event on the server, the page MUST display a clear "event not found" message — no partial data or error traces. @@ -90,15 +90,29 @@ A guest navigates to an event URL that no longer resolves — the event was dele ### Key Entities -- **Event**: Has a public event token (UUID in URL), title, optional description, date/time, optional location, expiry date, and optionally a cancelled state with message. -- **RSVP**: Has a guest name and attending status; confirmed attendees (status = attending) are listed on the public event page. +- **Event**: Has a public event token (UUID in URL), title, optional description, date/time (OffsetDateTime — displayed in the organizer's original timezone, no conversion to viewer timezone), IANA timezone name (e.g. `Europe/Berlin`, stored as separate field — required for human-readable timezone display), optional location, expiry date (LocalDate), and optionally a cancelled state with message. See `.specify/memory/research/datetime-best-practices.md` for full stack type mapping. + - **Note**: The IANA timezone requires a new `timezone` field on the Event entity and API schema. This impacts US-1 (Create Event) — the frontend must send the organizer's IANA zone ID alongside the OffsetDateTime. +- **RSVP**: Has a guest name and binary attending status (attending / not attending — no "maybe"). Only the count of confirmed attendees (status = attending) is exposed on the public event page. Individual names are visible only in the organizer view, sorted alphabetically by name. ## Success Criteria ### Measurable Outcomes - **SC-001**: A guest who opens a valid event URL can see all set event fields (title, date/time, and any optional fields) without logging in. -- **SC-002**: The attendee list and count reflect all current server-side RSVPs with attending status. +- **SC-002**: The attendee count reflects all current server-side RSVPs with attending status. No individual names are exposed on the public event page. - **SC-003**: An expired event URL renders the "ended" state — RSVP controls are absent from the DOM, not merely hidden via CSS. - **SC-004**: An unknown event token URL renders a "not found" message — no event data, no server error details. - **SC-005**: No network requests to external domains are made when loading the event page. + +## Clarifications + +### Session 2026-03-06 + +- Q: What should the event page display when the server is temporarily unavailable? → A: Generic friendly error state with a manual "Retry" button; no automatic retry. +- Q: How should date/time be displayed regarding timezones? → A: Organizer timezone preserved — display the time exactly as entered by the organizer (OffsetDateTime), no conversion to viewer's local timezone. The IANA timezone name (e.g. "Europe/Berlin") MUST be displayed alongside the time. Requires a new `timezone` field on Event entity/API (impacts US-1). +- Q: What is the URL pattern for event pages? → A: `/events/:token` (e.g. `/events/a1b2c3d4-...`). Plural, matching the RESTful API resource name. +- Q: Should guest names be visible to other guests on the public event page? → A: No. Only the attendee count is shown to guests. Individual names are exclusively visible to the organizer, sorted alphabetically. +- Q: How should the loading state look while the API call is in progress? → A: Skeleton-shimmer (placeholder blocks in field shape that shimmer until data arrives). +- Q: Should the event page include OpenGraph meta tags for link previews? → A: Out of scope for US-007. Separate user story — generic app-branding OG-tags only, no event data exposed to crawlers. Noted in `.specify/memory/ideen.md`. +- Q: Should date/time formatting adapt to the viewer's browser locale? → A: Yes, browser-locale-based via `Intl.DateTimeFormat` (e.g. DE: "15. März 2026, 20:00" / EN: "March 15, 2026, 8:00 PM"). +- Q: Is RSVP status binary or are there more states (e.g. "maybe")? → A: Binary — attending or not attending. No "maybe" status. Count reflects only confirmed attendees. diff --git a/specs/007-view-event/tasks.md b/specs/007-view-event/tasks.md new file mode 100644 index 0000000..06fdcc6 --- /dev/null +++ b/specs/007-view-event/tasks.md @@ -0,0 +1,225 @@ +# Tasks: View Event Landing Page + +**Input**: Design documents from `/specs/007-view-event/` +**Prerequisites**: plan.md, spec.md, research.md, data-model.md, contracts/get-event.yaml + +**Tests**: Included — constitution enforces Test-Driven Methodology (Principle II). + +**Organization**: Tasks grouped by user story. US3 (cancelled event) is deferred until US-18. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US4) +- Exact file paths included in descriptions + +--- + +## Phase 1: Setup (Cross-Cutting Schema Changes) + +**Purpose**: OpenAPI contract update, database migration, and type generation — prerequisites for all backend and frontend work. + +- [x] T001 Update OpenAPI spec: add `GET /events/{token}` endpoint, `GetEventResponse` schema, and `timezone` field to `CreateEventRequest`/`CreateEventResponse` in `backend/src/main/resources/openapi/api.yaml` +- [x] T002 [P] Add Liquibase changeset: `timezone VARCHAR(64) NOT NULL DEFAULT 'UTC'` column on `events` table in `backend/src/main/resources/db/changelog/` +- [x] T003 Regenerate frontend TypeScript types from updated OpenAPI spec in `frontend/src/api/schema.d.ts` + +**Checkpoint**: OpenAPI contract finalized, DB schema ready, frontend types available. + +--- + +## Phase 2: Foundational (Backend — Blocks All User Stories) + +**Purpose**: Domain model update, new GET use case, controller endpoint, and backend tests. All user stories depend on this. + +**CRITICAL**: No frontend user story work can begin until this phase is complete. + +### Backend Tests (TDD — write first, verify they fail) + +- [x] T004 [P] Backend unit tests for `GetEventUseCase`: test getByEventToken returns event, returns empty for unknown token, computes expired flag — in `backend/src/test/java/de/fete/` +- [x] T005 [P] Backend controller tests for `GET /events/{token}`: test 200 with full response, 200 with optional fields absent, 404 with ProblemDetail — in `backend/src/test/java/de/fete/` +- [x] T006 [P] Backend tests for timezone in Create Event flow: request validation (valid/invalid IANA zone), persistence round-trip — in `backend/src/test/java/de/fete/` + +### Backend Implementation + +- [x] T007 Add `timezone` field (String) to domain model in `backend/src/main/java/de/fete/domain/model/Event.java` +- [x] T008 [P] Add `timezone` column to JPA entity and update persistence mapping in `backend/src/main/java/de/fete/adapter/out/persistence/EventJpaEntity.java` and `EventPersistenceAdapter.java` +- [x] T009 [P] Update Create Event flow to accept and validate `timezone` (must be valid IANA zone ID via `ZoneId.getAvailableZoneIds()`) in `backend/src/main/java/de/fete/application/service/EventService.java` and `EventController.java` +- [x] T010 Create `GetEventUseCase` inbound port with `getByEventToken(UUID): Optional` in `backend/src/main/java/de/fete/domain/port/in/GetEventUseCase.java` +- [x] T011 Implement `GetEventUseCase` in `backend/src/main/java/de/fete/application/service/EventService.java` — delegates to existing `findByEventToken()` repository method +- [x] T012 Implement `getEvent()` in `backend/src/main/java/de/fete/adapter/in/web/EventController.java` — maps domain Event to GetEventResponse, computes `expired` (expiryDate vs server clock) and `attendeeCount` (hardcoded 0) + +**Checkpoint**: Backend complete — `GET /api/events/{token}` returns 200 or 404. All backend tests pass. + +--- + +## Phase 3: User Story 1 — View Event Details as Guest (Priority: P1) MVP + +**Goal**: A guest opens a shared event link and sees all event information: title, date/time with IANA timezone, description, location, attendee count. Loading shows skeleton shimmer. + +**Independent Test**: Navigate to a valid event URL, verify all fields display correctly with locale-formatted date/time. + +### Tests for User Story 1 + +- [x] T013 [P] [US1] Unit tests for EventDetailView loaded state: renders title, date/time (locale-formatted via `Intl.DateTimeFormat`), timezone, description, location, attendee count — in `frontend/src/__tests__/EventDetailView.spec.ts` +- [x] T014 [P] [US1] Unit test for EventDetailView loading state: renders skeleton shimmer placeholders — in `frontend/src/__tests__/EventDetailView.spec.ts` + +### Implementation for User Story 1 + +- [x] T015 [P] [US1] Add skeleton shimmer CSS (CSS-only gradient animation, no dependencies) in `frontend/src/assets/main.css` +- [x] T016 [US1] Create `EventDetailView.vue` with loading (skeleton shimmer) and loaded states — fetches event via `openapi-fetch` GET `/events/{token}`, formats date/time with `Intl.DateTimeFormat` using browser locale — in `frontend/src/views/EventDetailView.vue` +- [x] T017 [US1] Update router to use `EventDetailView` for `/events/:token` route in `frontend/src/router/index.ts` +- [x] T018 [P] [US1] Update `EventCreateView.vue` to send `timezone` field (auto-detected via `Intl.DateTimeFormat().resolvedOptions().timeZone`) in `frontend/src/views/EventCreateView.vue` +- [x] T019 [US1] E2E test for loaded event: navigate to valid event URL, verify all fields displayed, verify no external resource requests — in `frontend/e2e/event-view.spec.ts` + +**Checkpoint**: US1 complete — guest can view event details. Skeleton shimmer during loading. Date/time locale-formatted with timezone label. + +--- + +## Phase 4: User Story 2 — View Expired Event (Priority: P2) + +**Goal**: A guest opens a link to an expired event. The page shows event details plus a clear "event has ended" indicator. No RSVP actions shown. + +**Independent Test**: Create an event with past expiry date, navigate to its URL, verify "event has ended" state renders and no RSVP controls are present. + +**Dependencies**: Requires Phase 3 (EventDetailView exists). + +### Tests for User Story 2 + +- [x] T020 [P] [US2] Unit test for EventDetailView expired state: renders "event has ended" indicator, RSVP controls absent from DOM — in `frontend/src/__tests__/EventDetailView.spec.ts` + +### Implementation for User Story 2 + +- [x] T021 [US2] Add expired state rendering to `EventDetailView.vue`: show event details + "event has ended" banner when `expired === true`, no RSVP actions in DOM — in `frontend/src/views/EventDetailView.vue` +- [x] T022 [US2] E2E test for expired event: MSW returns event with `expired: true`, verify banner and absent RSVP controls — in `frontend/e2e/event-view.spec.ts` + +**Checkpoint**: US2 complete — expired events clearly show "ended" state. + +--- + +## Phase 5: User Story 4 — Event Not Found (Priority: P2) + +**Goal**: A guest navigates to an invalid event URL. The page shows a clear "event not found" message — no partial data, no error traces. + +**Independent Test**: Navigate to a URL with an unknown event token, verify "event not found" message renders. + +**Dependencies**: Requires Phase 3 (EventDetailView exists). No dependency on US2. + +### Tests for User Story 4 + +- [x] T023 [P] [US4] Unit test for EventDetailView not-found state: renders "event not found" message, no event data in DOM — in `frontend/src/__tests__/EventDetailView.spec.ts` + +### Implementation for User Story 4 + +- [x] T024 [US4] Add not-found state rendering to `EventDetailView.vue`: show "event not found" message when API returns 404 — in `frontend/src/views/EventDetailView.vue` +- [x] T025 [US4] E2E test for event not found: MSW returns 404 ProblemDetail, verify message and no event data — in `frontend/e2e/event-view.spec.ts` + +**Checkpoint**: US4 complete — invalid tokens show friendly not-found message. + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +**Purpose**: Server error edge case, final validation, and cleanup. + +- [x] T026 Add server error state with manual retry button to `EventDetailView.vue`: friendly error message + "Retry" button that re-fetches — in `frontend/src/views/EventDetailView.vue` +- [x] T027 [P] Unit test for server error + retry state in `frontend/src/__tests__/EventDetailView.spec.ts` +- [x] T028 [P] E2E test for server error: MSW returns 500, verify error message and retry button functionality — in `frontend/e2e/event-view.spec.ts` +- [x] T029 Run quickstart.md validation: verify all key changes listed in quickstart.md are implemented + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies — start immediately +- **Foundational (Phase 2)**: Depends on T001 (OpenAPI spec) and T002 (migration) from Setup +- **US1 (Phase 3)**: Depends on Phase 2 completion (backend endpoint must exist) +- **US2 (Phase 4)**: Depends on Phase 3 (EventDetailView exists) — can parallelize with US4 +- **US4 (Phase 5)**: Depends on Phase 3 (EventDetailView exists) — can parallelize with US2 +- **Polish (Phase 6)**: Depends on Phase 3 minimum; ideally after US2 + US4 + +### User Story Dependencies + +``` +Phase 1 (Setup) ──► Phase 2 (Backend) ──► Phase 3 (US1/MVP) + │ + ┌────┴────┐ + ▼ ▼ + Phase 4 Phase 5 + (US2) (US4) + └────┬────┘ + ▼ + Phase 6 (Polish) +``` + +- **US1 (P1)**: Requires Phase 2 — no dependency on other stories +- **US2 (P2)**: Requires US1 (same component) — no dependency on US4 +- **US4 (P2)**: Requires US1 (same component) — no dependency on US2 +- **US3 (P2)**: DEFERRED until US-18 (cancel event) is implemented + +### Within Each Phase + +- Tests MUST be written and FAIL before implementation (TDD) +- Models/ports before services +- Services before controllers +- Backend before frontend (for the same endpoint) + +### Parallel Opportunities + +**Phase 1**: T002 (migration) can run in parallel with T001 (OpenAPI update) +**Phase 2**: T004, T005, T006 (tests) can run in parallel. T008, T009 can run in parallel after T007. +**Phase 3**: T013, T014 (unit tests) and T015 (CSS) can run in parallel. T018 (create form timezone) is independent. +**Phase 4 + 5**: US2 and US4 can be implemented in parallel (different UI states, same file but non-conflicting sections). + +--- + +## Parallel Example: Phase 2 (Backend) + +```bash +# Write all backend tests in parallel (TDD): +Task T004: "Unit tests for GetEventUseCase" +Task T005: "Controller tests for GET /events/{token}" +Task T006: "Tests for timezone in Create Event flow" + +# Then implement in parallel where possible: +Task T008: "Add timezone to JPA entity + persistence" # parallel +Task T009: "Update Create Event flow for timezone" # parallel +# T010-T012 are sequential (port → service → controller) +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete Phase 1: Setup (OpenAPI + migration + types) +2. Complete Phase 2: Backend (domain + use case + controller + tests) +3. Complete Phase 3: US1 (EventDetailView + router + tests) +4. **STOP and VALIDATE**: Guest can view event details via shared link +5. Deploy/demo if ready + +### Incremental Delivery + +1. Setup + Backend → Backend ready, API testable via curl +2. Add US1 → Guest can view events (MVP!) +3. Add US2 → Expired events show "ended" state +4. Add US4 → Invalid tokens show "not found" +5. Polish → Server error handling, final validation + +### Deferred Work + +- **US3 (Cancelled event)**: Blocked on US-18. No tasks generated. Will require adding `cancelled` + `cancellationMessage` to GetEventResponse and a new UI state. + +--- + +## Notes + +- [P] tasks = different files, no dependencies on incomplete tasks +- [Story] label maps task to specific user story for traceability +- `attendeeCount` returns 0 until RSVP feature (US-8+) is implemented (R-3) +- `expired` is computed server-side using injected Clock bean (R-4) +- Frontend route: `/events/:token` — API path: `/api/events/{token}` (R-5) +- Skeleton shimmer is CSS-only, no additional dependencies (R-6) +- Date/time formatted via `Intl.DateTimeFormat` with browser locale (spec clarification Q7)