884 lines
19 KiB
Markdown
884 lines
19 KiB
Markdown
# Wedding Website Bootstrap Plan
|
|
|
|
## Goal
|
|
|
|
Build the dynamic foundation for a wedding website before final visual/content design is complete.
|
|
|
|
The website should support:
|
|
|
|
- A public wedding homepage with static sections and personalized guest content.
|
|
- Unique invitation links for each invited group/household.
|
|
- RSVP and per-person attendance/meal collection.
|
|
- Optional guest contributions such as song requests, toast requests, messages, and one photo upload.
|
|
- A private `/admin` portal restricted to the couple through Google login.
|
|
- A Docker Compose runtime with PostgreSQL and MinIO.
|
|
|
|
## Confirmed decisions
|
|
|
|
### Application stack
|
|
|
|
- Framework: Next.js App Router.
|
|
- Database: PostgreSQL.
|
|
- Query layer: Kysely, no ORM.
|
|
- Authentication: Google OAuth.
|
|
- Admin access: restricted by email allowlist from `.env`.
|
|
- Object storage: MinIO using S3-compatible APIs.
|
|
- Deployment/runtime style: Docker Compose.
|
|
- Admin UI component system: shadcn/ui only.
|
|
|
|
### Admin UI requirement
|
|
|
|
All UI components used in the admin area must come from shadcn/ui.
|
|
|
|
This applies to every admin page under:
|
|
|
|
```txt
|
|
/admin
|
|
/admin/*
|
|
```
|
|
|
|
The admin area must use shadcn/ui components for:
|
|
|
|
- layout primitives where applicable
|
|
- cards
|
|
- buttons
|
|
- forms
|
|
- inputs
|
|
- selects
|
|
- checkboxes
|
|
- dialogs
|
|
- dropdown menus
|
|
- tables
|
|
- badges
|
|
- tabs
|
|
- alerts
|
|
- toasts/sonner notifications
|
|
- action menus
|
|
- empty states where applicable
|
|
- loading/skeleton states
|
|
|
|
Custom admin UI should only be composed from shadcn/ui components and small local wrappers around them. Do not introduce another component library for the admin portal.
|
|
|
|
If a needed admin component is not yet installed from shadcn/ui, add it through the shadcn workflow and then reuse it consistently.
|
|
|
|
The guest-facing invitation pages may have custom wedding-themed design, but the admin management UI must remain shadcn-based.
|
|
|
|
### Invitation model
|
|
|
|
- The primary guest-facing unit is an `Invitation`, not an individual guest.
|
|
- An invitation represents a household, couple, family, individual guest, or group.
|
|
- Each invitation has one unique private token and one invite URL:
|
|
|
|
```txt
|
|
/invite/[token]
|
|
```
|
|
|
|
- Each invitation must have a required language.
|
|
- The invitation language is the source of truth for which language the guest-facing invitation page uses.
|
|
- This is a core requirement because many guests may only speak one of the wedding languages.
|
|
- Admins choose the invitation language when creating or editing an invitation.
|
|
- Admin pages can remain in the couple's preferred management language initially, but guest-facing copy must respect the invitation language.
|
|
|
|
### Invitation members
|
|
|
|
- Each invitation contains one or more `InvitationMember` records.
|
|
- Members are modeled from the start because attendance and food choices can differ per person.
|
|
- Per-person attendance lives on `invitation_members.attending`.
|
|
- The response table must not duplicate attendance status.
|
|
|
|
### RSVP/response model
|
|
|
|
- `invitation_members` owns per-person answers:
|
|
- attending
|
|
- meal preference
|
|
- dietary restrictions
|
|
- `invitation_responses` owns group-level logistics:
|
|
- submitted timestamp
|
|
- travel dates
|
|
- stay location
|
|
- transport help
|
|
- message to the couple
|
|
- RSVP edits are allowed until a configured deadline.
|
|
|
|
### Photo uploads
|
|
|
|
- One image per invitation/group.
|
|
- The image may be replaced until the deadline.
|
|
- Store files in MinIO.
|
|
- Store object metadata in PostgreSQL.
|
|
|
|
### Optional wedding interactions
|
|
|
|
Include these from the bootstrap if feasible:
|
|
|
|
- Song requests.
|
|
- Optional toast/speech request.
|
|
- Advice, wish, or favorite memory message.
|
|
|
|
## Proposed environment variables
|
|
|
|
```env
|
|
DATABASE_URL=postgres://wedding:wedding@localhost:5432/wedding
|
|
|
|
AUTH_SECRET=change-me
|
|
AUTH_GOOGLE_ID=change-me
|
|
AUTH_GOOGLE_SECRET=change-me
|
|
ADMIN_EMAILS=person-one@example.com,person-two@example.com
|
|
|
|
APP_URL=http://localhost:3000
|
|
RSVP_DEADLINE=2026-08-01
|
|
|
|
S3_ENDPOINT=http://localhost:9000
|
|
S3_REGION=us-east-1
|
|
S3_BUCKET=wedding-uploads
|
|
S3_ACCESS_KEY_ID=minioadmin
|
|
S3_SECRET_ACCESS_KEY=minioadmin
|
|
S3_FORCE_PATH_STYLE=true
|
|
```
|
|
|
|
## Target folder structure
|
|
|
|
```txt
|
|
app/
|
|
page.tsx
|
|
invite/
|
|
[token]/
|
|
page.tsx
|
|
admin/
|
|
page.tsx
|
|
guests/
|
|
page.tsx
|
|
new/
|
|
page.tsx
|
|
[id]/
|
|
page.tsx
|
|
api/
|
|
auth/
|
|
[...nextauth]/
|
|
route.ts
|
|
uploads/
|
|
presign/
|
|
route.ts
|
|
complete/
|
|
route.ts
|
|
|
|
lib/
|
|
auth.ts
|
|
config.ts
|
|
db/
|
|
client.ts
|
|
types.ts
|
|
queries/
|
|
invitations.ts
|
|
members.ts
|
|
responses.ts
|
|
uploads.ts
|
|
song-requests.ts
|
|
toast-requests.ts
|
|
guest-messages.ts
|
|
storage/
|
|
s3.ts
|
|
uploads.ts
|
|
i18n/
|
|
config.ts
|
|
dictionaries/
|
|
en.ts
|
|
es.ts
|
|
de.ts
|
|
validation/
|
|
invitation.ts
|
|
response.ts
|
|
upload.ts
|
|
interactions.ts
|
|
|
|
scripts/
|
|
migrate-sql.mjs
|
|
|
|
migrations/
|
|
0001_initial_schema.sql
|
|
|
|
docker-compose.yml
|
|
plans/
|
|
wedding-bootstrap-plan.md
|
|
```
|
|
|
|
## Database schema plan
|
|
|
|
### `invitations`
|
|
|
|
Represents one invite/group/household.
|
|
|
|
```txt
|
|
id uuid primary key
|
|
token text unique not null
|
|
display_name text not null
|
|
language text not null
|
|
contact_email text
|
|
contact_phone text
|
|
max_party_size integer not null default 1
|
|
allow_member_edits boolean not null default false
|
|
notes text
|
|
created_at timestamptz not null
|
|
updated_at timestamptz not null
|
|
```
|
|
|
|
Notes:
|
|
|
|
- `token` must be unguessable.
|
|
- `display_name` is what the guest sees, e.g. `Ana & Luis` or `The Garcia Family`.
|
|
- `language` controls guest-facing page copy and form labels for this invitation.
|
|
- Recommended initial language values should be short locale codes such as `en`, `es`, `de`, etc.
|
|
- `allow_member_edits` allows flexible +1 or family edits later.
|
|
|
|
### `invitation_members`
|
|
|
|
Represents each individual person under an invitation.
|
|
|
|
```txt
|
|
id uuid primary key
|
|
invitation_id uuid not null references invitations(id) on delete cascade
|
|
name text not null
|
|
is_primary_contact boolean not null default false
|
|
attending boolean
|
|
meal_preference text
|
|
dietary_restrictions text
|
|
notes text
|
|
created_at timestamptz not null
|
|
updated_at timestamptz not null
|
|
```
|
|
|
|
Notes:
|
|
|
|
- `attending = null` means pending/unanswered.
|
|
- Attendance status is derived from this table, not from `invitation_responses`.
|
|
- Meal preference examples: `meat`, `fish`, `vegetarian`, `vegan`, `kids`, `other`.
|
|
|
|
### `invitation_responses`
|
|
|
|
Stores group-level RSVP metadata and logistics.
|
|
|
|
```txt
|
|
id uuid primary key
|
|
invitation_id uuid unique not null references invitations(id) on delete cascade
|
|
submitted_at timestamptz
|
|
arrival_date date
|
|
departure_date date
|
|
stay_location text
|
|
coming_from text
|
|
needs_transport_help boolean
|
|
message_to_couple text
|
|
created_at timestamptz not null
|
|
updated_at timestamptz not null
|
|
```
|
|
|
|
Notes:
|
|
|
|
- No attendance status here.
|
|
- No attendee count here.
|
|
- Admin dashboard derives totals from `invitation_members`.
|
|
|
|
### `photo_uploads`
|
|
|
|
Stores one uploaded activity/surprise image per invitation.
|
|
|
|
```txt
|
|
id uuid primary key
|
|
invitation_id uuid unique not null references invitations(id) on delete cascade
|
|
bucket text not null
|
|
object_key text not null
|
|
original_filename text
|
|
content_type text
|
|
size_bytes integer
|
|
created_at timestamptz not null
|
|
updated_at timestamptz not null
|
|
```
|
|
|
|
Notes:
|
|
|
|
- Unique `invitation_id` enforces one photo per invitation.
|
|
- Replacing a photo updates this record and may delete/overwrite the old object.
|
|
|
|
### `song_requests`
|
|
|
|
Stores optional party song requests.
|
|
|
|
```txt
|
|
id uuid primary key
|
|
invitation_id uuid not null references invitations(id) on delete cascade
|
|
title text not null
|
|
artist text
|
|
note text
|
|
created_at timestamptz not null
|
|
```
|
|
|
|
Notes:
|
|
|
|
- Recommended limit: up to 3 song requests per invitation.
|
|
|
|
### `toast_requests`
|
|
|
|
Stores optional requests to say a few words.
|
|
|
|
```txt
|
|
id uuid primary key
|
|
invitation_id uuid not null references invitations(id) on delete cascade
|
|
member_id uuid references invitation_members(id) on delete set null
|
|
speaker_name text not null
|
|
estimated_minutes integer
|
|
note text
|
|
status text not null default 'pending'
|
|
created_at timestamptz not null
|
|
updated_at timestamptz not null
|
|
```
|
|
|
|
Recommended statuses:
|
|
|
|
```txt
|
|
pending
|
|
accepted
|
|
declined
|
|
scheduled
|
|
```
|
|
|
|
Notes:
|
|
|
|
- Toast requests are voluntary.
|
|
- Admin approval is required before scheduling.
|
|
|
|
### `guest_messages`
|
|
|
|
Stores optional advice, wishes, or memories.
|
|
|
|
```txt
|
|
id uuid primary key
|
|
invitation_id uuid not null references invitations(id) on delete cascade
|
|
type text not null
|
|
message text not null
|
|
created_at timestamptz not null
|
|
```
|
|
|
|
Recommended message types:
|
|
|
|
```txt
|
|
advice
|
|
memory
|
|
wish
|
|
```
|
|
|
|
### `admin_audit_events` optional
|
|
|
|
Useful but not required for the first pass.
|
|
|
|
```txt
|
|
id uuid primary key
|
|
admin_email text not null
|
|
action text not null
|
|
target_type text
|
|
target_id uuid
|
|
metadata jsonb
|
|
created_at timestamptz not null
|
|
```
|
|
|
|
## Derived RSVP status logic
|
|
|
|
Admin-facing status should be computed instead of stored redundantly.
|
|
|
|
Suggested derived statuses:
|
|
|
|
```txt
|
|
Pending
|
|
invitation_response.submitted_at is null
|
|
|
|
Partially answered
|
|
submitted_at exists and at least one member has attending = null
|
|
|
|
Attending
|
|
at least one member has attending = true and no members are pending
|
|
|
|
Declined
|
|
all members have attending = false and no members are pending
|
|
|
|
Partial
|
|
at least one member attending = true and at least one member attending = false
|
|
```
|
|
|
|
Dashboard metrics can derive:
|
|
|
|
- total invitations
|
|
- total invited members
|
|
- attending members
|
|
- declined members
|
|
- pending members
|
|
- invitations with submitted responses
|
|
- invitations with uploaded photos
|
|
- song request count
|
|
- pending toast requests
|
|
|
|
## Guest-facing invite flow
|
|
|
|
Route:
|
|
|
|
```txt
|
|
/invite/[token]
|
|
```
|
|
|
|
Flow:
|
|
|
|
1. Resolve invitation by token.
|
|
2. If token is invalid, show a friendly invalid-invitation page.
|
|
3. If valid, render personalized wedding page shell.
|
|
4. Load guest-facing copy based on `invitations.language`.
|
|
5. Show static wedding information:
|
|
- hero
|
|
- date
|
|
- countdown
|
|
- location
|
|
- accommodation information
|
|
- short couple story placeholder
|
|
6. Show RSVP section with all invitation members.
|
|
7. For each member:
|
|
- attending yes/no
|
|
- meal preference if attending
|
|
- dietary restrictions if attending
|
|
8. Show group logistics:
|
|
- arrival date
|
|
- departure date
|
|
- stay location
|
|
- coming from
|
|
- transport help
|
|
- message to the couple
|
|
9. Show optional interaction sections:
|
|
- song requests
|
|
- toast/speech request
|
|
- advice/wish/memory
|
|
- one photo upload
|
|
10. Before deadline, allow edits.
|
|
11. After deadline, show submitted answers read-only and explain that changes require contacting the couple.
|
|
|
|
Guest-facing localized content should include at least:
|
|
|
|
- page headings
|
|
- navigation/section labels
|
|
- RSVP form labels
|
|
- validation messages
|
|
- success/error messages
|
|
- deadline/read-only messages
|
|
- upload instructions
|
|
- optional interaction prompts
|
|
|
|
Wedding factual content, such as venue names and addresses, can remain shared, but surrounding explanatory copy should be localized.
|
|
|
|
## Admin portal plan
|
|
|
|
Admin routes:
|
|
|
|
```txt
|
|
/admin
|
|
/admin/guests
|
|
/admin/guests/new
|
|
/admin/guests/[id]
|
|
```
|
|
|
|
Although the domain model is `invitations`, the UI can still use guest-friendly labels like `Guests`.
|
|
|
|
### `/admin`
|
|
|
|
Dashboard cards:
|
|
|
|
- Total invitations.
|
|
- Total invited people.
|
|
- RSVP pending.
|
|
- Attending people.
|
|
- Declined people.
|
|
- Uploaded photos.
|
|
- Song requests.
|
|
- Pending toast requests.
|
|
|
|
### `/admin/guests`
|
|
|
|
Invitation list table:
|
|
|
|
- display name
|
|
- language
|
|
- contact email
|
|
- contact phone
|
|
- members count
|
|
- derived RSVP status
|
|
- attending count
|
|
- invite link
|
|
- last updated
|
|
|
|
Actions:
|
|
|
|
- copy invite link
|
|
- view details
|
|
- edit
|
|
|
|
### `/admin/guests/new`
|
|
|
|
Create invitation form:
|
|
|
|
- display name
|
|
- language
|
|
- contact email
|
|
- contact phone
|
|
- max party size
|
|
- allow member edits
|
|
- notes
|
|
- initial members
|
|
|
|
On submit:
|
|
|
|
- generate token
|
|
- create invitation
|
|
- create initial members
|
|
- show generated invite URL
|
|
|
|
The language field is required.
|
|
|
|
### `/admin/guests/[id]`
|
|
|
|
Detail page:
|
|
|
|
- invitation metadata
|
|
- invite token/link
|
|
- member list and per-person RSVP answers
|
|
- group logistics
|
|
- uploaded photo preview/link
|
|
- song requests
|
|
- toast requests with admin status controls
|
|
- advice/wish/memory messages
|
|
- admin notes
|
|
|
|
Admin actions:
|
|
|
|
- edit invitation
|
|
- add/edit/remove members if needed
|
|
- regenerate invite token
|
|
- manually adjust RSVP/member data
|
|
- accept/decline/schedule toast request
|
|
|
|
## Authentication plan
|
|
|
|
Use Google OAuth with an email allowlist.
|
|
|
|
Implementation targets:
|
|
|
|
```txt
|
|
auth.ts
|
|
app/api/auth/[...nextauth]/route.ts
|
|
middleware.ts
|
|
```
|
|
|
|
Rules:
|
|
|
|
- Only emails listed in `ADMIN_EMAILS` may access `/admin` and `/admin/*`.
|
|
- Non-admin Google users are rejected or shown a forbidden page.
|
|
- Guest invite pages do not require login.
|
|
- Admin emails remain in `.env` for the bootstrap.
|
|
|
|
Before implementation, verify the exact current Next.js/Auth.js setup for the installed framework version.
|
|
|
|
## MinIO upload plan
|
|
|
|
Use AWS SDK S3-compatible client.
|
|
|
|
Upload flow:
|
|
|
|
1. Guest selects one image.
|
|
2. Client asks the app for a presigned upload URL.
|
|
3. App validates invitation token and deadline.
|
|
4. App creates an object key:
|
|
|
|
```txt
|
|
invitations/{invitationId}/{uuid}-{safeFilename}
|
|
```
|
|
|
|
5. Browser uploads directly to MinIO using presigned PUT.
|
|
6. Client calls complete endpoint.
|
|
7. App upserts `photo_uploads` metadata for the invitation.
|
|
|
|
Validation:
|
|
|
|
- Allow only image MIME types:
|
|
- `image/jpeg`
|
|
- `image/png`
|
|
- `image/webp`
|
|
- Enforce a max size.
|
|
- Enforce one current photo per invitation.
|
|
|
|
Admin viewing:
|
|
|
|
- Generate short-lived signed GET URLs for previews/downloads.
|
|
|
|
## Docker Compose plan
|
|
|
|
Services:
|
|
|
|
```txt
|
|
app
|
|
postgres
|
|
minio
|
|
minio-init
|
|
```
|
|
|
|
Postgres:
|
|
|
|
- expose `5432`
|
|
- database: `wedding`
|
|
- user: `wedding`
|
|
- password: `wedding`
|
|
|
|
MinIO:
|
|
|
|
- API: `9000`
|
|
- console: `9001`
|
|
- bucket: `wedding-uploads`
|
|
|
|
`minio-init`:
|
|
|
|
- wait for MinIO
|
|
- create bucket if it does not exist
|
|
|
|
## Implementation sequence
|
|
|
|
### Step 1: Review framework-specific docs
|
|
|
|
Because this project uses Next.js 16, review the locally installed Next documentation before coding framework-specific pieces.
|
|
|
|
Focus areas:
|
|
|
|
- App Router route handlers.
|
|
- Server actions/forms.
|
|
- Middleware behavior.
|
|
- Authentication integration compatibility.
|
|
|
|
### Step 2: Add dependencies
|
|
|
|
Install likely packages:
|
|
|
|
```bash
|
|
npm install kysely pg zod next-auth @aws-sdk/client-s3 @aws-sdk/s3-request-presigner nanoid
|
|
npm install -D @types/pg kysely-codegen
|
|
```
|
|
|
|
Adjust if current package versions require different auth package names or setup.
|
|
|
|
### Step 3: Add environment handling
|
|
|
|
- Create `.env.example`.
|
|
- Add typed config helper in `lib/config.ts`.
|
|
- Validate required env vars at startup/server usage.
|
|
|
|
### Step 4: Add Docker Compose
|
|
|
|
- Add `docker-compose.yml`.
|
|
- Define Postgres service.
|
|
- Define MinIO service.
|
|
- Define MinIO bucket initialization.
|
|
- Optionally define app service.
|
|
|
|
### Step 5: Add Kysely database client
|
|
|
|
- Create `lib/db/client.ts`.
|
|
- Create initial manual DB types in `lib/db/types.ts`.
|
|
- Add SQL migration runner in `scripts/migrate-sql.mjs`.
|
|
- Add package script:
|
|
|
|
```json
|
|
"db:migrate": "node scripts/migrate-sql.mjs"
|
|
```
|
|
|
|
### Step 6: Create initial SQL migration
|
|
|
|
Create `migrations/0001_initial_schema.sql` for:
|
|
|
|
- `invitations`
|
|
- `invitation_members`
|
|
- `invitation_responses`
|
|
- `photo_uploads`
|
|
- `song_requests`
|
|
- `toast_requests`
|
|
- `guest_messages`
|
|
- optional `admin_audit_events`
|
|
|
|
### Step 7: Add query modules
|
|
|
|
Create query helpers for:
|
|
|
|
- invitations
|
|
- invitation members
|
|
- invitation responses
|
|
- uploads
|
|
- song requests
|
|
- toast requests
|
|
- guest messages
|
|
|
|
Keep SQL explicit and typed through Kysely.
|
|
|
|
### Step 8: Add validation schemas
|
|
|
|
Use Zod for:
|
|
|
|
- creating/updating invitations
|
|
- creating/updating members
|
|
- submitting RSVP/member attendance
|
|
- submitting logistics
|
|
- song requests
|
|
- toast requests
|
|
- guest messages
|
|
- upload presign/complete requests
|
|
|
|
Invitation validation must require a supported language code.
|
|
|
|
### Step 9: Implement Google admin auth
|
|
|
|
- Configure Google provider.
|
|
- Restrict sign-in to `ADMIN_EMAILS`.
|
|
- Protect `/admin` routes.
|
|
- Add sign-in/sign-out UI as needed.
|
|
|
|
### Step 10: Build admin MVP
|
|
|
|
Implement:
|
|
|
|
- `/admin` dashboard.
|
|
- `/admin/guests` invitation list.
|
|
- `/admin/guests/new` create form.
|
|
- `/admin/guests/[id]` detail page.
|
|
|
|
Minimum admin operations:
|
|
|
|
- create invitation
|
|
- add members
|
|
- copy invite link
|
|
- view submitted RSVP data
|
|
- view optional guest interactions
|
|
|
|
### Step 11: Build guest invite MVP
|
|
|
|
Implement `/invite/[token]` with:
|
|
|
|
- invalid token handling
|
|
- personalized greeting
|
|
- language-aware guest-facing copy based on `invitations.language`
|
|
- wedding info shell
|
|
- countdown
|
|
- member attendance form
|
|
- per-member meal/dietary fields
|
|
- group logistics form
|
|
- submission/edit behavior until deadline
|
|
|
|
### Step 12: Add optional guest interactions
|
|
|
|
Add form sections for:
|
|
|
|
- up to 3 song requests
|
|
- optional toast/speech request
|
|
- advice/wish/memory message
|
|
|
|
Admin should be able to view all of them.
|
|
|
|
### Step 13: Add MinIO photo upload
|
|
|
|
Implement:
|
|
|
|
- S3 client.
|
|
- presigned PUT endpoint.
|
|
- upload complete endpoint.
|
|
- one-photo-per-invitation metadata upsert.
|
|
- admin signed preview/download URL.
|
|
|
|
### Step 14: Add basic design shell
|
|
|
|
Replace default starter UI with a simple wedding-appropriate layout:
|
|
|
|
- elegant typography
|
|
- hero section
|
|
- countdown
|
|
- location/accommodation placeholders
|
|
- story placeholder
|
|
- clear guest forms
|
|
- responsive mobile-first layout
|
|
|
|
Final visual design can remain future work.
|
|
|
|
### Step 14.5: Add localization foundation
|
|
|
|
Add a simple localization layer for guest-facing invitation pages.
|
|
|
|
Recommended bootstrap approach:
|
|
|
|
- Keep supported language codes in config.
|
|
- Store the selected language on each invitation.
|
|
- Resolve guest-facing copy from `invitations.language` on `/invite/[token]`.
|
|
- Create translation dictionaries for guest-facing copy.
|
|
- Fallback to a default language only if necessary, but admin should always select a supported language.
|
|
|
|
Suggested structure:
|
|
|
|
```txt
|
|
lib/
|
|
i18n/
|
|
config.ts
|
|
dictionaries/
|
|
en.ts
|
|
es.ts
|
|
de.ts
|
|
```
|
|
|
|
Do not rely only on browser language detection for invited guests. The invitation language is the source of truth.
|
|
|
|
### Step 15: Verification
|
|
|
|
Run:
|
|
|
|
```bash
|
|
npm run lint
|
|
npm run build
|
|
```
|
|
|
|
Manual happy path:
|
|
|
|
1. Start Docker Compose.
|
|
2. Run migrations.
|
|
3. Log in as admin with allowed Google account.
|
|
4. Create an invitation with multiple members.
|
|
5. Set the invitation language.
|
|
6. Copy invite link.
|
|
7. Open invite link.
|
|
8. Confirm the page renders in the invitation's configured language.
|
|
9. Submit attendance with different meal preferences per member.
|
|
10. Add travel details.
|
|
11. Add song request, toast request, and message.
|
|
12. Upload one photo.
|
|
13. Return to admin and confirm all data is visible.
|
|
14. Test invalid invite token.
|
|
15. Test non-admin access to `/admin`.
|
|
16. Test deadline read-only behavior.
|
|
|
|
## Open implementation questions
|
|
|
|
These can be decided during implementation:
|
|
|
|
1. Should guests be able to add/edit member names when `allow_member_edits = true`, or only fill unnamed placeholders?
|
|
2. What should the exact RSVP deadline be?
|
|
3. What max upload size should be allowed?
|
|
4. Should admin be able to export guest/RSVP data as CSV in the first version?
|
|
5. Which exact languages should be supported in the first version?
|
|
|
|
## Recommended first milestone
|
|
|
|
The first useful milestone should be:
|
|
|
|
- Docker Compose with Postgres and MinIO.
|
|
- Initial pure SQL migration executed by `scripts/migrate-sql.mjs`.
|
|
- Google admin login restricted by `.env` emails.
|
|
- Admin can create invitation with members.
|
|
- Admin can select a language per invitation.
|
|
- Guest can open `/invite/[token]` and submit per-member RSVP answers.
|
|
- Guest invite page renders in the invitation's configured language.
|
|
- Admin can see submitted answers.
|
|
|
|
After that, add photo uploads and the fun interaction sections.
|