This commit is contained in:
@@ -0,0 +1,883 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user