- Move cross-cutting docs (personas, design system, implementation phases, Ideen.md) to .specify/memory/ - Move cross-cutting research and plans to .specify/memory/research/ and .specify/memory/plans/ - Extract 5 setup tasks from spec/setup-tasks.md into individual specs/001-005/spec.md files with spec-kit template format - Extract 20 user stories from spec/userstories.md into individual specs/006-026/spec.md files with spec-kit template format - Relocate feature-specific research and plan docs into specs/[feature]/ - Add spec-kit constitution, templates, scripts, and slash commands - Slim down CLAUDE.md to Claude-Code-specific config, delegate principles to .specify/memory/constitution.md - Update ralph.sh with stream-json output and per-iteration logging - Delete old spec/ and docs/agents/ directories - Gitignore Ralph iteration JSONL logs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1153 lines
44 KiB
Markdown
1153 lines
44 KiB
Markdown
---
|
|
date: 2026-03-04T21:41:13+00:00
|
|
git_commit: 91e566efea0cbf53ba06a29b63317b7435609bd8
|
|
branch: master
|
|
topic: "US-1: Create an Event"
|
|
tags: [plan, us-1, event-creation, full-stack]
|
|
status: approved
|
|
---
|
|
|
|
# US-1: Create an Event — Implementation Plan
|
|
|
|
## Overview
|
|
|
|
Implement the first user story end-to-end: an organizer creates an event with title, description, date/time, location, and expiry date. The server stores the event, returns event token + organizer token. The frontend stores tokens in localStorage and redirects to a stub event page. This is the first vertical slice through all layers of the hexagonal architecture.
|
|
|
|
Additionally: remove the scaffolding health endpoint from the OpenAPI spec (replaced by Spring Actuator), establish RFC 9457 error handling, set the app's visual design foundation (mobile-first, app-native feel), and clean up Vue scaffold defaults.
|
|
|
|
## Current State Analysis
|
|
|
|
- All setup tasks (T-1 through T-5) complete
|
|
- Hexagonal architecture skeleton with ArchUnit enforcement — all business packages empty
|
|
- OpenAPI spec contains only a scaffolding `/health` endpoint (to be removed)
|
|
- Liquibase with empty baseline migration
|
|
- Testcontainers PostgreSQL for integration tests
|
|
- Vue 3 SPA with placeholder routes and openapi-fetch client
|
|
- Dockerfile HEALTHCHECK correctly uses `/actuator/health` (not the custom endpoint)
|
|
- No domain code, no persistence, no controllers exist yet
|
|
|
|
### Key Discoveries:
|
|
- `WebConfig.java:19` — API prefix `/api` applied to all `@RestController` beans
|
|
- `WebConfig.java:23-39` — SPA fallback routes non-API requests to `index.html`
|
|
- `application.properties:4` — JPA `ddl-auto=validate` (Liquibase manages schema)
|
|
- OpenAPI generator: `interfaceOnly=true`, `useBeanValidation=true`, packages `de.fete.adapter.in.web.api` / `.model`
|
|
- Frontend types generated via `npm run generate:api` into `src/api/schema.d.ts`
|
|
- Dockerfile HEALTHCHECK (`Dockerfile:25-26`) uses `/actuator/health` — safe to remove custom health endpoint
|
|
|
|
## Desired End State
|
|
|
|
After this plan is complete:
|
|
|
|
1. `POST /api/events` accepts a JSON body with title, description, dateTime, location, expiryDate
|
|
2. Server validates input (Bean Validation), creates the event with two UUIDs (event token, organizer token), persists to PostgreSQL, returns both tokens
|
|
3. Error responses follow RFC 9457 Problem Details format with field-level validation errors
|
|
4. Frontend shows a mobile-first event creation form at `/create`
|
|
5. On success, localStorage stores organizer token + event metadata, browser redirects to `/events/:token` (stub page)
|
|
6. Root page `/` shows a minimal landing with "Create Event" button
|
|
7. The scaffolding health endpoint is removed from the OpenAPI spec
|
|
8. Vue scaffold defaults are cleaned up
|
|
9. All tests pass (backend unit + integration, frontend unit, ArchUnit, Checkstyle)
|
|
|
|
### UI Mockups
|
|
|
|
#### Root Page `/` (Mobile)
|
|
```
|
|
┌─────────────────────────────┐
|
|
│ │
|
|
│ ┌─────────────────────┐ │
|
|
│ │ fete │ │
|
|
│ └─────────────────────┘ │
|
|
│ │
|
|
│ │
|
|
│ No events yet. │
|
|
│ Create your first one! │
|
|
│ │
|
|
│ ┌─────────────────────┐ │
|
|
│ │ + Create Event │ │
|
|
│ └─────────────────────┘ │
|
|
│ │
|
|
│ │
|
|
└─────────────────────────────┘
|
|
```
|
|
|
|
#### Create Event `/create` (Mobile)
|
|
```
|
|
┌─────────────────────────────┐
|
|
│ ← Create │
|
|
│ │
|
|
│ ┌─────────────────────────┐│
|
|
│ │ Title * ││
|
|
│ │ ││
|
|
│ └─────────────────────────┘│
|
|
│ │
|
|
│ ┌─────────────────────────┐│
|
|
│ │ Description ││
|
|
│ │ ││
|
|
│ │ ││
|
|
│ └─────────────────────────┘│
|
|
│ │
|
|
│ ┌─────────────────────────┐│
|
|
│ │ 📅 Date & Time * ││
|
|
│ └─────────────────────────┘│
|
|
│ │
|
|
│ ┌─────────────────────────┐│
|
|
│ │ 📍 Location ││
|
|
│ └─────────────────────────┘│
|
|
│ │
|
|
│ ┌─────────────────────────┐│
|
|
│ │ 📆 Expiry Date * ││
|
|
│ └─────────────────────────┘│
|
|
│ │
|
|
│ ┌─────────────────────────┐│
|
|
│ │ Create Event ││
|
|
│ └─────────────────────────┘│
|
|
│ │
|
|
└─────────────────────────────┘
|
|
|
|
* = required
|
|
Card-style inputs, rounded corners,
|
|
generous padding, gradient background
|
|
```
|
|
|
|
#### Event Stub `/events/:token` (Mobile)
|
|
```
|
|
┌─────────────────────────────┐
|
|
│ ← fete │
|
|
│ │
|
|
│ │
|
|
│ ✓ Event created! │
|
|
│ │
|
|
│ Share this link: │
|
|
│ ┌─────────────────────────┐│
|
|
│ │ https://…/events/abc123 ││
|
|
│ │ 📋 Copy ││
|
|
│ └─────────────────────────┘│
|
|
│ │
|
|
│ │
|
|
└─────────────────────────────┘
|
|
```
|
|
|
|
#### Desktop Layout
|
|
```
|
|
┌──────────────────────────────────────────────┐
|
|
│ ┌──────────────────┐ │
|
|
│ │ │ │
|
|
│ │ (mobile view │ │
|
|
│ │ centered, │ │
|
|
│ │ max ~480px) │ │
|
|
│ │ │ │
|
|
│ └──────────────────┘ │
|
|
│ │
|
|
│ ← gradient background fills full width → │
|
|
└──────────────────────────────────────────────┘
|
|
```
|
|
|
|
## What We're NOT Doing
|
|
|
|
- Full event page (US-2) — only a stub with "Event created" confirmation
|
|
- RSVP functionality (US-3)
|
|
- Organizer view / event editing (US-4, US-5)
|
|
- Dark/light mode toggle (US-17) — but we design CSS with future theming in mind
|
|
- Concrete color palette selection — researched during implementation with browser tools
|
|
- PWA manifest / service worker (US-14)
|
|
- Event image or color theme (US-15, US-16)
|
|
|
|
## Design Principles (Persistent — Applies to All Future Stories)
|
|
|
|
These design decisions apply to US-1 and all subsequent frontend work:
|
|
|
|
- **Mobile-first / App-native feel** — not a classic website. Think installed app, not browser page.
|
|
- **Desktop:** centered narrow column (max ~480px), gradient background fills the rest
|
|
- **Gradient backgrounds** as primary design language (subtle 2-3 color gradients)
|
|
- **Card-style form fields** — rounded corners, generous padding, elevated look
|
|
- **Typography: Sora** — contemporary geometric sans-serif with slightly rounded terminals. Self-hosted WOFF2, OFL 1.1 licensed. Source: github.com/sora-xor/sora-font. No Google Fonts CDN.
|
|
- **Generous whitespace** — elements breathe, nothing cramped
|
|
- **WCAG AA contrast** as baseline for all color choices
|
|
|
|
### Color Palette: Electric Dusk
|
|
|
|
Chosen for best balance of style, broad appeal, and accessibility (white text readable across almost the entire gradient).
|
|
|
|
| Role | Hex | Description |
|
|
|------|-----|-------------|
|
|
| Gradient Start | `#F06292` | Pink |
|
|
| Gradient Mid | `#AB47BC` | Purple |
|
|
| Gradient End | `#5C6BC0` | Indigo blue |
|
|
| Accent (CTAs/buttons) | `#FF7043` | Deep orange |
|
|
| Text (light mode) | `#1C1C1E` | Near black |
|
|
| Text (dark mode) | `#FFFFFF` | White |
|
|
| Surface (light) | `#FFF5F8` | Pinkish white |
|
|
| Surface (dark) | `#1B1730` | Deep indigo-black |
|
|
| Card (light) | `#FFFFFF` | White |
|
|
| Card (dark) | `#2A2545` | Muted indigo |
|
|
|
|
**Primary gradient:**
|
|
```css
|
|
background: linear-gradient(135deg, #F06292 0%, #AB47BC 50%, #5C6BC0 100%);
|
|
```
|
|
|
|
**Usage rules:**
|
|
- Gradient for hero/splash areas and page backgrounds — not as direct text background for body copy
|
|
- Cards and content areas use solid surface colors with high-contrast text
|
|
- Accent color (`#FF7043`) for primary action buttons with dark text (`#1C1C1E`)
|
|
- White text on gradient mid/end passes WCAG AA (4.82:1 and 4.86:1)
|
|
- White text on gradient start passes AA-large (3.06:1) — use for headings 18px+ only
|
|
|
|
## Implementation Approach
|
|
|
|
API-first, TDD, inside-out through the hexagonal layers. Seven phases:
|
|
|
|
1. **OpenAPI Spec & Cleanup** — define the contract, remove scaffolding health endpoint
|
|
2. **Database Migration** — create the `events` table
|
|
3. **Domain & Ports** — `Event` model, `CreateEventUseCase`, `EventRepository`
|
|
4. **Application Service** — `EventService` implementing the use case
|
|
5. **Persistence Adapter** — JPA entity, Spring Data repository
|
|
6. **Web Adapter & Error Handling** — Controller, `GlobalExceptionHandler`
|
|
7. **Frontend** — design foundation, form, localStorage, routing, stub page
|
|
|
|
---
|
|
|
|
## Phase 1: OpenAPI Spec & Cleanup
|
|
|
|
### Overview
|
|
Define the `POST /events` contract in the OpenAPI spec. Remove the scaffolding `/health` endpoint and `HealthResponse` schema. Verify that nothing depends on the removed endpoint. Regenerate backend interfaces and frontend types.
|
|
|
|
### Changes Required:
|
|
|
|
#### [x] 1.1 Remove health endpoint from OpenAPI spec
|
|
**File**: `backend/src/main/resources/openapi/api.yaml`
|
|
**Changes**: Remove the `/health` path and `HealthResponse` schema. Add the `POST /events` path with request/response schemas and RFC 9457 error response schemas.
|
|
|
|
```yaml
|
|
openapi: 3.1.0
|
|
info:
|
|
title: fete API
|
|
description: Privacy-focused event announcements and RSVPs
|
|
version: 0.1.0
|
|
license:
|
|
name: GPL-3.0-or-later
|
|
identifier: GPL-3.0-or-later
|
|
|
|
servers:
|
|
- url: /api
|
|
|
|
paths:
|
|
/events:
|
|
post:
|
|
operationId: createEvent
|
|
summary: Create a new event
|
|
tags:
|
|
- events
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CreateEventRequest"
|
|
responses:
|
|
"201":
|
|
description: Event created successfully
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: "#/components/schemas/CreateEventResponse"
|
|
"400":
|
|
description: Validation failed
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: "#/components/schemas/ValidationProblemDetail"
|
|
|
|
components:
|
|
schemas:
|
|
CreateEventRequest:
|
|
type: object
|
|
required:
|
|
- title
|
|
- dateTime
|
|
- expiryDate
|
|
properties:
|
|
title:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
description:
|
|
type: string
|
|
maxLength: 2000
|
|
dateTime:
|
|
type: string
|
|
format: date-time
|
|
description: Event date and time with UTC offset (ISO 8601)
|
|
example: "2026-03-15T20:00:00+01:00"
|
|
location:
|
|
type: string
|
|
maxLength: 500
|
|
expiryDate:
|
|
type: string
|
|
format: date
|
|
description: Date after which event data is deleted. Must be in the future.
|
|
example: "2026-06-15"
|
|
|
|
CreateEventResponse:
|
|
type: object
|
|
required:
|
|
- eventToken
|
|
- organizerToken
|
|
- title
|
|
- dateTime
|
|
- expiryDate
|
|
properties:
|
|
eventToken:
|
|
type: string
|
|
format: uuid
|
|
description: Public token for the event URL
|
|
organizerToken:
|
|
type: string
|
|
format: uuid
|
|
description: Secret token for organizer access
|
|
title:
|
|
type: string
|
|
dateTime:
|
|
type: string
|
|
format: date-time
|
|
expiryDate:
|
|
type: string
|
|
format: date
|
|
|
|
ProblemDetail:
|
|
type: object
|
|
properties:
|
|
type:
|
|
type: string
|
|
format: uri
|
|
default: "about:blank"
|
|
title:
|
|
type: string
|
|
status:
|
|
type: integer
|
|
detail:
|
|
type: string
|
|
instance:
|
|
type: string
|
|
format: uri
|
|
additionalProperties: true
|
|
|
|
ValidationProblemDetail:
|
|
allOf:
|
|
- $ref: "#/components/schemas/ProblemDetail"
|
|
- type: object
|
|
properties:
|
|
fieldErrors:
|
|
type: array
|
|
items:
|
|
type: object
|
|
required:
|
|
- field
|
|
- message
|
|
properties:
|
|
field:
|
|
type: string
|
|
message:
|
|
type: string
|
|
```
|
|
|
|
#### [x] 1.2 Remove health endpoint test reference
|
|
**File**: `backend/src/test/java/de/fete/config/WebConfigTest.java`
|
|
**Changes**: Verify that tests only reference `/actuator/health`, not `/api/health`. Remove or update any test that calls the generated `HealthApi` interface. The test `apiPrefixNotAccessibleWithoutIt` may need to be adapted to use the new `/events` endpoint instead.
|
|
|
|
#### [x] 1.3 Regenerate backend and frontend types
|
|
**Action**: Run `cd backend && ./mvnw compile` and `cd frontend && npm run generate:api` to verify generation succeeds with the new spec. The old `HealthApi.java` and `HealthResponse.java` in `target/` will be replaced by `EventsApi.java`, `CreateEventRequest.java`, `CreateEventResponse.java`, etc.
|
|
|
|
### Success Criteria:
|
|
|
|
#### Automated Verification:
|
|
- [ ] `cd backend && ./mvnw compile` succeeds — generates `EventsApi` interface, request/response models
|
|
- [ ] `cd frontend && npm run generate:api` succeeds — `schema.d.ts` contains event types
|
|
- [ ] `cd backend && ./mvnw test` passes — no test references removed health endpoint
|
|
- [ ] No `HealthApi.java` or `HealthResponse.java` in generated output
|
|
|
|
#### Manual Verification:
|
|
- [ ] OpenAPI spec is valid (no syntax errors)
|
|
- [ ] Generated `EventsApi` interface has `createEvent` method with correct signature
|
|
|
|
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
|
|
|
|
---
|
|
|
|
## Phase 2: Database Migration
|
|
|
|
### Overview
|
|
Create the `events` table via a Liquibase migration. The table stores all event fields plus both UUID tokens and a `created_at` audit timestamp.
|
|
|
|
### Changes Required:
|
|
|
|
#### [x] 2.1 Create Liquibase migration
|
|
**File**: `backend/src/main/resources/db/changelog/001-create-events-table.xml`
|
|
**Changes**: New migration file.
|
|
|
|
```xml
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<databaseChangeLog
|
|
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
|
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
|
|
|
|
<changeSet id="001-create-events-table" author="fete">
|
|
<createTable tableName="events">
|
|
<column name="id" type="bigserial" autoIncrement="true">
|
|
<constraints primaryKey="true" nullable="false"/>
|
|
</column>
|
|
<column name="event_token" type="uuid">
|
|
<constraints nullable="false" unique="true"/>
|
|
</column>
|
|
<column name="organizer_token" type="uuid">
|
|
<constraints nullable="false" unique="true"/>
|
|
</column>
|
|
<column name="title" type="varchar(200)">
|
|
<constraints nullable="false"/>
|
|
</column>
|
|
<column name="description" type="varchar(2000)"/>
|
|
<column name="date_time" type="timestamptz">
|
|
<constraints nullable="false"/>
|
|
</column>
|
|
<column name="location" type="varchar(500)"/>
|
|
<column name="expiry_date" type="date">
|
|
<constraints nullable="false"/>
|
|
</column>
|
|
<column name="created_at" type="timestamptz" defaultValueComputed="now()">
|
|
<constraints nullable="false"/>
|
|
</column>
|
|
</createTable>
|
|
|
|
<createIndex tableName="events" indexName="idx_events_event_token">
|
|
<column name="event_token"/>
|
|
</createIndex>
|
|
|
|
<createIndex tableName="events" indexName="idx_events_expiry_date">
|
|
<column name="expiry_date"/>
|
|
</createIndex>
|
|
</changeSet>
|
|
|
|
</databaseChangeLog>
|
|
```
|
|
|
|
#### [x] 2.2 Include migration in master changelog
|
|
**File**: `backend/src/main/resources/db/changelog/db.changelog-master.xml`
|
|
**Changes**: Add include for the new migration file after the baseline.
|
|
|
|
```xml
|
|
<include file="db/changelog/001-create-events-table.xml"/>
|
|
```
|
|
|
|
### Success Criteria:
|
|
|
|
#### Automated Verification:
|
|
- [ ] `cd backend && ./mvnw test` passes — Testcontainers spins up PostgreSQL, Liquibase runs migration, Hibernate validates schema
|
|
- [ ] Index on `event_token` exists (primary lookup path for public access)
|
|
- [ ] Index on `expiry_date` exists (cleanup job query path, US-12)
|
|
|
|
#### Manual Verification:
|
|
- [ ] Migration file uses correct PostgreSQL types (`timestamptz`, `date`, `uuid`, `bigserial`)
|
|
- [ ] Column constraints match domain rules (title not null, description nullable, etc.)
|
|
|
|
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
|
|
|
|
---
|
|
|
|
## Phase 3: Domain Model & Ports
|
|
|
|
### Overview
|
|
Create the `Event` domain entity and the inbound/outbound port interfaces. The domain layer has zero dependencies on Spring or adapters (enforced by ArchUnit).
|
|
|
|
### Changes Required:
|
|
|
|
#### [x] 3.1 Event domain model
|
|
**File**: `backend/src/main/java/de/fete/domain/model/Event.java`
|
|
**Changes**: New file. Plain Java class — no JPA annotations, no Spring dependencies.
|
|
|
|
```java
|
|
package de.fete.domain.model;
|
|
|
|
import java.time.LocalDate;
|
|
import java.time.OffsetDateTime;
|
|
import java.util.UUID;
|
|
|
|
public class Event {
|
|
|
|
private Long id;
|
|
private UUID eventToken;
|
|
private UUID organizerToken;
|
|
private String title;
|
|
private String description;
|
|
private OffsetDateTime dateTime;
|
|
private String location;
|
|
private LocalDate expiryDate;
|
|
private OffsetDateTime createdAt;
|
|
|
|
// Constructor, getters, setters (or builder pattern if warranted)
|
|
}
|
|
```
|
|
|
|
#### [x] 3.2 CreateEventUseCase inbound port
|
|
**File**: `backend/src/main/java/de/fete/domain/port/in/CreateEventUseCase.java`
|
|
**Changes**: New file. Interface with a command object.
|
|
|
|
```java
|
|
package de.fete.domain.port.in;
|
|
|
|
import de.fete.domain.model.Event;
|
|
import java.time.LocalDate;
|
|
import java.time.OffsetDateTime;
|
|
|
|
public interface CreateEventUseCase {
|
|
|
|
Event createEvent(CreateEventCommand command);
|
|
|
|
record CreateEventCommand(
|
|
String title,
|
|
String description,
|
|
OffsetDateTime dateTime,
|
|
String location,
|
|
LocalDate expiryDate
|
|
) {}
|
|
}
|
|
```
|
|
|
|
#### [x] 3.3 EventRepository outbound port
|
|
**File**: `backend/src/main/java/de/fete/domain/port/out/EventRepository.java`
|
|
**Changes**: New file. Interface — only the `save` method needed for US-1. `findByEventToken` added for the stub page route (minimal, but needed for redirect verification in later stories).
|
|
|
|
```java
|
|
package de.fete.domain.port.out;
|
|
|
|
import de.fete.domain.model.Event;
|
|
import java.util.Optional;
|
|
import java.util.UUID;
|
|
|
|
public interface EventRepository {
|
|
|
|
Event save(Event event);
|
|
|
|
Optional<Event> findByEventToken(UUID eventToken);
|
|
}
|
|
```
|
|
|
|
### Success Criteria:
|
|
|
|
#### Automated Verification:
|
|
- [ ] `cd backend && ./mvnw test` passes — ArchUnit validates:
|
|
- `Event` in `domain.model` has no Spring/adapter dependencies
|
|
- `CreateEventUseCase` and `EventRepository` are interfaces
|
|
- No dependency violations between layers
|
|
- [ ] `cd backend && ./mvnw checkstyle:check` passes
|
|
|
|
#### Manual Verification:
|
|
- [ ] Domain model fields match the database schema from Phase 2
|
|
- [ ] `CreateEventCommand` record contains only the fields the organizer provides (no tokens, no id, no createdAt)
|
|
|
|
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
|
|
|
|
---
|
|
|
|
## Phase 4: Application Service
|
|
|
|
### Overview
|
|
Implement `EventService` — the use case implementation that validates the expiry date, generates UUID tokens, builds the domain model, and delegates to the repository.
|
|
|
|
### Changes Required:
|
|
|
|
#### [x] 4.1 EventService
|
|
**File**: `backend/src/main/java/de/fete/application/service/EventService.java`
|
|
**Changes**: New file. Implements `CreateEventUseCase`. Spring `@Service` annotation.
|
|
|
|
```java
|
|
package de.fete.application.service;
|
|
|
|
import de.fete.domain.model.Event;
|
|
import de.fete.domain.port.in.CreateEventUseCase;
|
|
import de.fete.domain.port.out.EventRepository;
|
|
import java.time.LocalDate;
|
|
import java.time.OffsetDateTime;
|
|
import java.util.UUID;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
@Service
|
|
public class EventService implements CreateEventUseCase {
|
|
|
|
private final EventRepository eventRepository;
|
|
|
|
public EventService(EventRepository eventRepository) {
|
|
this.eventRepository = eventRepository;
|
|
}
|
|
|
|
@Override
|
|
public Event createEvent(CreateEventCommand command) {
|
|
if (!command.expiryDate().isAfter(LocalDate.now())) {
|
|
throw new ExpiryDateInPastException(command.expiryDate());
|
|
}
|
|
|
|
var event = new Event();
|
|
event.setEventToken(UUID.randomUUID());
|
|
event.setOrganizerToken(UUID.randomUUID());
|
|
event.setTitle(command.title());
|
|
event.setDescription(command.description());
|
|
event.setDateTime(command.dateTime());
|
|
event.setLocation(command.location());
|
|
event.setExpiryDate(command.expiryDate());
|
|
event.setCreatedAt(OffsetDateTime.now());
|
|
|
|
return eventRepository.save(event);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### [x] 4.2 ExpiryDateInPastException
|
|
**File**: `backend/src/main/java/de/fete/application/service/ExpiryDateInPastException.java`
|
|
**Changes**: New file. Domain-level exception for the business rule "expiry date must be in the future".
|
|
|
|
```java
|
|
package de.fete.application.service;
|
|
|
|
import java.time.LocalDate;
|
|
|
|
public class ExpiryDateInPastException extends RuntimeException {
|
|
|
|
private final LocalDate expiryDate;
|
|
|
|
public ExpiryDateInPastException(LocalDate expiryDate) {
|
|
super("Expiry date must be in the future: " + expiryDate);
|
|
this.expiryDate = expiryDate;
|
|
}
|
|
|
|
public LocalDate getExpiryDate() {
|
|
return expiryDate;
|
|
}
|
|
}
|
|
```
|
|
|
|
#### [x] 4.3 Unit tests for EventService
|
|
**File**: `backend/src/test/java/de/fete/application/service/EventServiceTest.java`
|
|
**Changes**: New file. Unit tests with a mocked `EventRepository`.
|
|
|
|
Test cases:
|
|
- Happy path: valid command → event saved with generated tokens, createdAt set
|
|
- Expiry date today → `ExpiryDateInPastException`
|
|
- Expiry date in the past → `ExpiryDateInPastException`
|
|
- Expiry date tomorrow → succeeds
|
|
- Event token and organizer token are different UUIDs
|
|
- Repository `save` is called exactly once
|
|
|
|
### Success Criteria:
|
|
|
|
#### Automated Verification:
|
|
- [ ] `cd backend && ./mvnw test` passes — all `EventServiceTest` tests green
|
|
- [ ] `cd backend && ./mvnw checkstyle:check` passes
|
|
- [ ] ArchUnit: `EventService` in `application.service` may depend on `domain.model` and `domain.port` but not on adapters
|
|
|
|
#### Manual Verification:
|
|
- [ ] Business rule enforced: expiry date must be strictly after today
|
|
- [ ] UUID generation uses `UUID.randomUUID()` (v4, non-guessable)
|
|
|
|
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
|
|
|
|
---
|
|
|
|
## Phase 5: Persistence Adapter
|
|
|
|
### Overview
|
|
Implement the JPA entity and Spring Data repository that fulfill the `EventRepository` outbound port. The adapter translates between the domain model and the JPA entity.
|
|
|
|
### Changes Required:
|
|
|
|
#### [x] 5.1 JPA Entity
|
|
**File**: `backend/src/main/java/de/fete/adapter/out/persistence/EventJpaEntity.java`
|
|
**Changes**: New file. JPA entity mapping to the `events` table.
|
|
|
|
```java
|
|
package de.fete.adapter.out.persistence;
|
|
|
|
import jakarta.persistence.*;
|
|
import java.time.LocalDate;
|
|
import java.time.OffsetDateTime;
|
|
import java.util.UUID;
|
|
|
|
@Entity
|
|
@Table(name = "events")
|
|
public class EventJpaEntity {
|
|
|
|
@Id
|
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
private Long id;
|
|
|
|
@Column(name = "event_token", nullable = false, unique = true)
|
|
private UUID eventToken;
|
|
|
|
@Column(name = "organizer_token", nullable = false, unique = true)
|
|
private UUID organizerToken;
|
|
|
|
@Column(nullable = false, length = 200)
|
|
private String title;
|
|
|
|
@Column(length = 2000)
|
|
private String description;
|
|
|
|
@Column(name = "date_time", nullable = false)
|
|
private OffsetDateTime dateTime;
|
|
|
|
@Column(length = 500)
|
|
private String location;
|
|
|
|
@Column(name = "expiry_date", nullable = false)
|
|
private LocalDate expiryDate;
|
|
|
|
@Column(name = "created_at", nullable = false)
|
|
private OffsetDateTime createdAt;
|
|
|
|
// Getters and setters
|
|
}
|
|
```
|
|
|
|
#### [x] 5.2 Spring Data Repository
|
|
**File**: `backend/src/main/java/de/fete/adapter/out/persistence/EventJpaRepository.java`
|
|
**Changes**: New file. Spring Data JPA interface.
|
|
|
|
```java
|
|
package de.fete.adapter.out.persistence;
|
|
|
|
import java.util.Optional;
|
|
import java.util.UUID;
|
|
import org.springframework.data.jpa.repository.JpaRepository;
|
|
|
|
public interface EventJpaRepository extends JpaRepository<EventJpaEntity, Long> {
|
|
|
|
Optional<EventJpaEntity> findByEventToken(UUID eventToken);
|
|
}
|
|
```
|
|
|
|
#### [x] 5.3 Persistence Adapter (Port Implementation)
|
|
**File**: `backend/src/main/java/de/fete/adapter/out/persistence/EventPersistenceAdapter.java`
|
|
**Changes**: New file. Implements `EventRepository` port, translates between domain model and JPA entity.
|
|
|
|
```java
|
|
package de.fete.adapter.out.persistence;
|
|
|
|
import de.fete.domain.model.Event;
|
|
import de.fete.domain.port.out.EventRepository;
|
|
import java.util.Optional;
|
|
import java.util.UUID;
|
|
import org.springframework.stereotype.Repository;
|
|
|
|
@Repository
|
|
public class EventPersistenceAdapter implements EventRepository {
|
|
|
|
private final EventJpaRepository jpaRepository;
|
|
|
|
public EventPersistenceAdapter(EventJpaRepository jpaRepository) {
|
|
this.jpaRepository = jpaRepository;
|
|
}
|
|
|
|
@Override
|
|
public Event save(Event event) {
|
|
EventJpaEntity entity = toEntity(event);
|
|
EventJpaEntity saved = jpaRepository.save(entity);
|
|
return toDomain(saved);
|
|
}
|
|
|
|
@Override
|
|
public Optional<Event> findByEventToken(UUID eventToken) {
|
|
return jpaRepository.findByEventToken(eventToken).map(this::toDomain);
|
|
}
|
|
|
|
private EventJpaEntity toEntity(Event event) { /* field mapping */ }
|
|
private Event toDomain(EventJpaEntity entity) { /* field mapping */ }
|
|
}
|
|
```
|
|
|
|
#### [x] 5.4 Integration test
|
|
**File**: `backend/src/test/java/de/fete/adapter/out/persistence/EventPersistenceAdapterTest.java`
|
|
**Changes**: New file. Integration test with Testcontainers.
|
|
|
|
Test cases:
|
|
- Save event → returns event with generated ID
|
|
- Save event → `findByEventToken` returns same event
|
|
- `findByEventToken` with unknown UUID → empty Optional
|
|
- All fields round-trip correctly (especially `OffsetDateTime` ↔ `timestamptz` and `LocalDate` ↔ `date`)
|
|
|
|
### Success Criteria:
|
|
|
|
#### Automated Verification:
|
|
- [ ] `cd backend && ./mvnw test` passes — persistence integration tests green
|
|
- [ ] `cd backend && ./mvnw checkstyle:check` passes
|
|
- [ ] ArchUnit: persistence adapter does not depend on web adapter
|
|
- [ ] Hibernate schema validation passes (JPA entity matches Liquibase migration)
|
|
|
|
#### Manual Verification:
|
|
- [ ] JPA entity column mappings match Liquibase migration exactly
|
|
- [ ] Domain ↔ JPA entity mapping is complete (no fields missed)
|
|
|
|
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
|
|
|
|
---
|
|
|
|
## Phase 6: Web Adapter & Error Handling
|
|
|
|
### Overview
|
|
Implement the REST controller (generated `EventsApi` interface) and the `GlobalExceptionHandler` for RFC 9457 Problem Details. Remove or update any remaining health endpoint references in tests.
|
|
|
|
### Changes Required:
|
|
|
|
#### [x] 6.1 GlobalExceptionHandler
|
|
**File**: `backend/src/main/java/de/fete/adapter/in/web/GlobalExceptionHandler.java`
|
|
**Changes**: New file. Handles all exceptions consistently as RFC 9457 Problem Details.
|
|
|
|
```java
|
|
package de.fete.adapter.in.web;
|
|
|
|
import de.fete.application.service.ExpiryDateInPastException;
|
|
import java.net.URI;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import org.springframework.http.*;
|
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
|
import org.springframework.web.context.request.WebRequest;
|
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
|
|
|
@RestControllerAdvice
|
|
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
|
|
|
|
@Override
|
|
protected ResponseEntity<Object> handleMethodArgumentNotValid(
|
|
MethodArgumentNotValidException ex,
|
|
HttpHeaders headers,
|
|
HttpStatusCode status,
|
|
WebRequest request) {
|
|
|
|
ProblemDetail problemDetail = ex.getBody();
|
|
problemDetail.setTitle("Validation Failed");
|
|
problemDetail.setType(URI.create("urn:problem-type:validation-error"));
|
|
|
|
List<Map<String, String>> fieldErrors = ex.getBindingResult()
|
|
.getFieldErrors()
|
|
.stream()
|
|
.map(fe -> Map.of(
|
|
"field", fe.getField(),
|
|
"message", fe.getDefaultMessage()
|
|
))
|
|
.toList();
|
|
|
|
problemDetail.setProperty("fieldErrors", fieldErrors);
|
|
return handleExceptionInternal(ex, problemDetail, headers, status, request);
|
|
}
|
|
|
|
@ExceptionHandler(ExpiryDateInPastException.class)
|
|
public ResponseEntity<ProblemDetail> handleExpiryDateInPast(
|
|
ExpiryDateInPastException ex) {
|
|
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
|
|
HttpStatus.BAD_REQUEST, ex.getMessage());
|
|
problemDetail.setTitle("Invalid Expiry Date");
|
|
problemDetail.setType(URI.create("urn:problem-type:expiry-date-in-past"));
|
|
return ResponseEntity.badRequest()
|
|
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
|
|
.body(problemDetail);
|
|
}
|
|
|
|
@ExceptionHandler(Exception.class)
|
|
public ResponseEntity<ProblemDetail> handleAll(Exception ex) {
|
|
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
|
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
|
"An unexpected error occurred.");
|
|
problemDetail.setTitle("Internal Server Error");
|
|
return ResponseEntity.internalServerError()
|
|
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
|
|
.body(problemDetail);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### [x] 6.2 EventController
|
|
**File**: `backend/src/main/java/de/fete/adapter/in/web/EventController.java`
|
|
**Changes**: New file. Implements generated `EventsApi` interface.
|
|
|
|
```java
|
|
package de.fete.adapter.in.web;
|
|
|
|
import de.fete.adapter.in.web.api.EventsApi;
|
|
import de.fete.adapter.in.web.model.CreateEventRequest;
|
|
import de.fete.adapter.in.web.model.CreateEventResponse;
|
|
import de.fete.domain.model.Event;
|
|
import de.fete.domain.port.in.CreateEventUseCase;
|
|
import de.fete.domain.port.in.CreateEventUseCase.CreateEventCommand;
|
|
import org.springframework.http.HttpStatus;
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
|
|
@RestController
|
|
public class EventController implements EventsApi {
|
|
|
|
private final CreateEventUseCase createEventUseCase;
|
|
|
|
public EventController(CreateEventUseCase createEventUseCase) {
|
|
this.createEventUseCase = createEventUseCase;
|
|
}
|
|
|
|
@Override
|
|
public ResponseEntity<CreateEventResponse> createEvent(
|
|
CreateEventRequest request) {
|
|
var command = new CreateEventCommand(
|
|
request.getTitle(),
|
|
request.getDescription(),
|
|
request.getDateTime(),
|
|
request.getLocation(),
|
|
request.getExpiryDate()
|
|
);
|
|
|
|
Event event = createEventUseCase.createEvent(command);
|
|
|
|
var response = new CreateEventResponse();
|
|
response.setEventToken(event.getEventToken());
|
|
response.setOrganizerToken(event.getOrganizerToken());
|
|
response.setTitle(event.getTitle());
|
|
response.setDateTime(event.getDateTime());
|
|
response.setExpiryDate(event.getExpiryDate());
|
|
|
|
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### [x] 6.3 Update WebConfigTest
|
|
**File**: `backend/src/test/java/de/fete/config/WebConfigTest.java`
|
|
**Changes**: Replace health endpoint references with the new `/api/events` endpoint. Keep the `/actuator/health` test. Adapt `apiPrefixNotAccessibleWithoutIt` to use `/events` instead of `/health`.
|
|
|
|
#### [x] 6.4 Integration test for EventController
|
|
**File**: `backend/src/test/java/de/fete/adapter/in/web/EventControllerIntegrationTest.java`
|
|
**Changes**: New file. Full Spring Boot integration test with MockMvc + Testcontainers.
|
|
|
|
Test cases:
|
|
- POST valid event → 201, response contains eventToken, organizerToken, title, dateTime, expiryDate
|
|
- POST missing title → 400, Problem Details with fieldErrors
|
|
- POST missing dateTime → 400, Problem Details with fieldErrors
|
|
- POST missing expiryDate → 400, Problem Details with fieldErrors
|
|
- POST expiry date in the past → 400, Problem Details with "expiry-date-in-past" type
|
|
- POST expiry date today → 400
|
|
- POST with all optional fields null → 201 (description and location are optional)
|
|
- Response Content-Type for errors is `application/problem+json`
|
|
|
|
#### [x] 6.5 Remove HealthController test (if any exists)
|
|
**File**: Verify no test file references a `HealthController` or `HealthApi` implementation. The only health tests should reference `/actuator/health`.
|
|
|
|
### Success Criteria:
|
|
|
|
#### Automated Verification:
|
|
- [ ] `cd backend && ./mvnw test` passes — all controller integration tests green
|
|
- [ ] `cd backend && ./mvnw checkstyle:check` passes
|
|
- [ ] `cd backend && ./mvnw verify` passes (full verification including SpotBugs)
|
|
- [ ] ArchUnit: web adapter does not depend on persistence adapter
|
|
- [ ] Error responses use `application/problem+json` content type
|
|
|
|
#### Manual Verification:
|
|
- [ ] `POST /api/events` with valid body returns 201 with both tokens
|
|
- [ ] Validation errors return field-level details in RFC 9457 format
|
|
- [ ] No `/api/health` endpoint exists anymore (404)
|
|
- [ ] `/actuator/health` still works
|
|
|
|
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
|
|
|
|
---
|
|
|
|
## Phase 7: Frontend
|
|
|
|
### Overview
|
|
Establish the visual design foundation (mobile-first app feel, gradients, card-style inputs, self-hosted font). Clean up Vue scaffold defaults. Implement the event creation form, localStorage composable, routing, and stub event page.
|
|
|
|
### Changes Required:
|
|
|
|
#### [x] 7.1 Clean up Vue scaffold defaults
|
|
**Files to remove**:
|
|
- `frontend/src/components/HelloWorld.vue`
|
|
- `frontend/src/components/TheWelcome.vue`
|
|
- `frontend/src/components/WelcomeItem.vue`
|
|
- `frontend/src/components/icons/*.vue` (all icon components)
|
|
- `frontend/src/views/AboutView.vue`
|
|
- `frontend/src/assets/base.css` (replaced by our own styles)
|
|
- `frontend/src/assets/logo.svg` (if exists)
|
|
- `frontend/src/components/__tests__/HelloWorld.spec.ts`
|
|
|
|
**Files to update**:
|
|
- `frontend/src/App.vue` — strip to minimal app shell
|
|
- `frontend/src/views/HomeView.vue` — replace with minimal landing (see mockup)
|
|
- `frontend/src/router/index.ts` — remove `/about` route
|
|
- `frontend/src/assets/main.css` — replace with our design foundation
|
|
|
|
#### [x] 7.2 Self-hosted font: Sora
|
|
**Directory**: `frontend/src/assets/fonts/`
|
|
**Changes**: Download Sora font from github.com/sora-xor/sora-font as WOFF2 files. Include weights: 400 (Regular), 500 (Medium), 600 (SemiBold), 700 (Bold), 800 (ExtraBold). Reference via `@font-face` in CSS. No external CDN. License: OFL 1.1.
|
|
|
|
#### [x] 7.3 Design foundation CSS (per `spec/design-system.md`)
|
|
**File**: `frontend/src/assets/main.css`
|
|
**Changes**: Establish the design system:
|
|
- CSS custom properties for colors, gradients, spacing, border-radius, font
|
|
- Mobile-first base styles
|
|
- Gradient background
|
|
- Card-style input components
|
|
- Desktop centered-column layout (max-width ~480px)
|
|
- Responsive breakpoints
|
|
- WCAG AA contrast (verified during implementation with color tools)
|
|
|
|
#### [x] 7.4 App shell component
|
|
**File**: `frontend/src/App.vue`
|
|
**Changes**: Minimal app shell with `<router-view>`. Gradient background wrapper. Centered column for desktop.
|
|
|
|
#### [x] 7.5 Home page (minimal landing)
|
|
**File**: `frontend/src/views/HomeView.vue`
|
|
**Changes**: Empty state with "Create Event" button linking to `/create`. Placeholder for future US-7 event overview.
|
|
|
|
#### [x] 7.6 Event creation form
|
|
**File**: `frontend/src/views/EventCreateView.vue`
|
|
**Changes**: New file. Form with card-style inputs for all fields:
|
|
- Title (required, text input)
|
|
- Description (optional, textarea)
|
|
- Date & Time (required, datetime-local input)
|
|
- Location (optional, text input)
|
|
- Expiry Date (required, date input, min=tomorrow)
|
|
|
|
Client-side validation:
|
|
- Required fields enforced (HTML5 `required` + JS check)
|
|
- Expiry date must be in the future (JS check)
|
|
- Show validation errors inline on the card fields
|
|
|
|
On submit:
|
|
- Call `api.POST('/events', { body })` via openapi-fetch client
|
|
- On success: store tokens in localStorage, redirect to `/events/:token`
|
|
- On error: display field errors from RFC 9457 response
|
|
|
|
#### [x] 7.7 localStorage composable
|
|
**File**: `frontend/src/composables/useEventStorage.ts`
|
|
**Changes**: New file. Composable for managing event data in localStorage.
|
|
|
|
```typescript
|
|
// Stores per event token:
|
|
// - organizerToken (if created from this device)
|
|
// - title, dateTime, expiryDate (for local overview, US-7)
|
|
|
|
interface StoredEvent {
|
|
eventToken: string
|
|
organizerToken?: string
|
|
title: string
|
|
dateTime: string
|
|
expiryDate: string
|
|
}
|
|
|
|
export function useEventStorage() {
|
|
function saveCreatedEvent(event: StoredEvent): void { /* ... */ }
|
|
function getStoredEvents(): StoredEvent[] { /* ... */ }
|
|
function getOrganizerToken(eventToken: string): string | undefined { /* ... */ }
|
|
// ...
|
|
}
|
|
```
|
|
|
|
Storage key pattern: `fete:events` → JSON array of `StoredEvent`.
|
|
|
|
#### [x] 7.8 Stub event page
|
|
**File**: `frontend/src/views/EventStubView.vue`
|
|
**Changes**: New file. Minimal confirmation page after event creation. Shows:
|
|
- "Event created!" confirmation
|
|
- Shareable event URL with copy-to-clipboard button
|
|
- Back link to home
|
|
|
|
This is a temporary stub — US-2 replaces it with the full event page.
|
|
|
|
#### [x] 7.9 Router update
|
|
**File**: `frontend/src/router/index.ts`
|
|
**Changes**: Replace scaffold routes with:
|
|
|
|
```typescript
|
|
const routes = [
|
|
{ path: '/', name: 'home', component: HomeView },
|
|
{ path: '/create', name: 'create-event', component: () => import('../views/EventCreateView.vue') },
|
|
{ path: '/events/:token', name: 'event', component: () => import('../views/EventStubView.vue') },
|
|
]
|
|
```
|
|
|
|
#### [x] 7.10 Frontend tests
|
|
**Files**:
|
|
- `frontend/src/composables/__tests__/useEventStorage.spec.ts` — localStorage composable tests
|
|
- `frontend/src/views/__tests__/EventCreateView.spec.ts` — form rendering, validation, submit flow
|
|
|
|
Test cases for localStorage composable:
|
|
- `saveCreatedEvent` stores data retrievable by `getStoredEvents`
|
|
- `getOrganizerToken` returns token for known event, undefined for unknown
|
|
- Multiple events stored independently
|
|
|
|
Test cases for EventCreateView:
|
|
- Renders all form fields
|
|
- Required fields have `required` attribute
|
|
- Submit button exists
|
|
- Form validation prevents submit with empty required fields
|
|
- (API call mocking for submit flow)
|
|
|
|
### Success Criteria:
|
|
|
|
#### Automated Verification:
|
|
- [ ] `cd frontend && npm run test:unit` passes — all new tests green
|
|
- [ ] `cd frontend && npm run build` succeeds — no TypeScript errors, no build errors
|
|
- [ ] `cd frontend && npx eslint src/` passes — no lint errors
|
|
- [ ] No references to removed scaffold components remain
|
|
|
|
#### Manual Verification:
|
|
- [ ] Home page shows "Create Event" button on mobile viewport
|
|
- [ ] Create form renders with card-style inputs, gradient background
|
|
- [ ] Desktop view centers content in narrow column
|
|
- [ ] Submitting a valid form creates the event and redirects to stub page
|
|
- [ ] Stub page shows shareable link with copy button
|
|
- [ ] localStorage contains event data after creation
|
|
- [ ] Validation errors display inline on the form (both client-side and server-side)
|
|
- [ ] Self-hosted font loads (no external requests in Network tab)
|
|
|
|
**Implementation Note**: After completing this phase, run the full verification suite, then use the `browser-interactive-testing` skill for visual verification of the form, desktop/mobile layouts, and the complete creation flow.
|
|
|
|
---
|
|
|
|
## Testing Strategy
|
|
|
|
### Unit Tests:
|
|
- `EventServiceTest` — business logic, token generation, expiry validation
|
|
- `useEventStorage.spec.ts` — localStorage operations
|
|
- `EventCreateView.spec.ts` — form rendering, validation
|
|
|
|
### Integration Tests:
|
|
- `EventPersistenceAdapterTest` — JPA ↔ PostgreSQL round-trip (Testcontainers)
|
|
- `EventControllerIntegrationTest` — full HTTP request/response cycle (MockMvc + Testcontainers)
|
|
|
|
### Architecture Tests:
|
|
- Existing ArchUnit tests validate all hexagonal layer boundaries automatically
|
|
|
|
### Manual Testing Steps:
|
|
1. Start backend + frontend in dev mode
|
|
2. Navigate to `/` — see landing page with "Create Event" button
|
|
3. Navigate to `/create` — fill out form, submit
|
|
4. Verify redirect to `/events/:token` stub page
|
|
5. Check localStorage for stored event data
|
|
6. Submit invalid form — verify error messages
|
|
7. Check Network tab — no external requests (fonts, CDNs, etc.)
|
|
8. Test on mobile viewport — verify app-native feel
|
|
|
|
## Performance Considerations
|
|
|
|
- UUID indexes on `event_token` and `organizer_token` for O(log n) lookups
|
|
- `expiry_date` index prepares for US-12 cleanup queries
|
|
- Lazy-loaded route components (`EventCreateView`, `EventStubView`) for code splitting
|
|
- Self-hosted font: WOFF2 format for minimal file size
|
|
|
|
## References
|
|
|
|
- `docs/agents/research/2026-03-04-us1-create-event.md` — US-1 codebase research
|
|
- `docs/agents/research/2026-03-04-rfc9457-problem-details.md` — Error handling research
|
|
- `docs/agents/research/2026-03-04-datetime-best-practices.md` — Date/time type mapping
|
|
- `spec/design-system.md` — Permanent design spec (palette, font, component patterns)
|
|
- `spec/userstories.md:21-41` — US-1 specification
|
|
- `spec/implementation-phases.md` — Implementation order
|