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>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
94
specs/007-view-event/contracts/get-event.yaml
Normal file
94
specs/007-view-event/contracts/get-event.yaml
Normal file
@@ -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"
|
||||
56
specs/007-view-event/data-model.md
Normal file
56
specs/007-view-event/data-model.md
Normal file
@@ -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` |
|
||||
89
specs/007-view-event/plan.md
Normal file
89
specs/007-view-event/plan.md
Normal file
@@ -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.
|
||||
39
specs/007-view-event/quickstart.md
Normal file
39
specs/007-view-event/quickstart.md
Normal file
@@ -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).
|
||||
100
specs/007-view-event/research.md
Normal file
100
specs/007-view-event/research.md
Normal file
@@ -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<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.
|
||||
@@ -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.
|
||||
|
||||
225
specs/007-view-event/tasks.md
Normal file
225
specs/007-view-event/tasks.md
Normal file
@@ -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<Event>` 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)
|
||||
Reference in New Issue
Block a user