This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
# Milestone 01 — Infrastructure and Database Foundation
|
||||
|
||||
## Reference
|
||||
|
||||
Read `plans/wedding-bootstrap-plan.md` first. It is the source of truth for architecture, naming, schema intent, and constraints.
|
||||
|
||||
## Goal
|
||||
|
||||
Create the local/runtime foundation for the app: dependencies, Docker Compose services, environment template, Kysely database client, SQL migration script, and initial schema.
|
||||
|
||||
## Scope
|
||||
|
||||
Implement only:
|
||||
|
||||
- Required dependencies for PostgreSQL, Kysely, migrations, validation, auth/upload placeholders where needed.
|
||||
- `docker-compose.yml` with PostgreSQL and MinIO services, plus MinIO bucket initialization.
|
||||
- `.env.example` with all required bootstrap variables.
|
||||
- Typed config helper.
|
||||
- Kysely PostgreSQL client.
|
||||
- Node `.mjs` SQL migration runner under `scripts/`.
|
||||
- Initial pure SQL migration for the core schema.
|
||||
- Initial database typings, either hand-written or generated if practical.
|
||||
- Package scripts needed to run migrations.
|
||||
|
||||
## Non-goals
|
||||
|
||||
Do not implement:
|
||||
|
||||
- Google auth.
|
||||
- `/admin` pages.
|
||||
- shadcn UI setup unless strictly required by project initialization.
|
||||
- Guest RSVP pages.
|
||||
- MinIO upload endpoints.
|
||||
- Presigned upload logic.
|
||||
- Song/toast/message forms.
|
||||
- Final visual design.
|
||||
|
||||
## Required constraints
|
||||
|
||||
- Use PostgreSQL.
|
||||
- Use Kysely. Do not add Prisma, Drizzle, TypeORM, Sequelize, or another ORM.
|
||||
- Keep SQL schema aligned with `wedding-bootstrap-plan.md`.
|
||||
- `invitations.language` is required from the first migration.
|
||||
- `invitation_members.attending` is the source of truth for attendance.
|
||||
- Do not duplicate attendance status on `invitation_responses`.
|
||||
- `photo_uploads.invitation_id` must be unique to enforce one photo per invitation.
|
||||
|
||||
## Implementation tasks
|
||||
|
||||
- Add dependencies:
|
||||
- `kysely`
|
||||
- `pg`
|
||||
- `zod`
|
||||
- `nanoid`
|
||||
- AWS S3 packages if useful to install now
|
||||
- Add dev dependencies:
|
||||
- `@types/pg`
|
||||
- `kysely-codegen` if used
|
||||
- Add `.env.example`.
|
||||
- Add `docker-compose.yml` with:
|
||||
- `postgres`
|
||||
- `minio`
|
||||
- `minio-init`
|
||||
- Add `lib/config.ts` for server-side env parsing.
|
||||
- Add `lib/db/client.ts`.
|
||||
- Add `scripts/migrate-sql.mjs`.
|
||||
- Add `migrations/0001_initial_schema.sql`.
|
||||
- Add `lib/db/types.ts`.
|
||||
- Add package scripts:
|
||||
- `db:migrate`
|
||||
- optionally `db:generate` if using `kysely-codegen`
|
||||
|
||||
## Initial tables
|
||||
|
||||
Create:
|
||||
|
||||
- `invitations`
|
||||
- `invitation_members`
|
||||
- `invitation_responses`
|
||||
- `photo_uploads`
|
||||
- `song_requests`
|
||||
- `toast_requests`
|
||||
- `guest_messages`
|
||||
- optionally `admin_audit_events`
|
||||
|
||||
## Verification
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
docker compose up -d postgres minio minio-init
|
||||
npm run db:migrate
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
If Docker is unavailable in the execution environment, still implement the files and clearly report that Docker verification could not be run.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Docker Compose defines working PostgreSQL and MinIO services.
|
||||
- `.env.example` documents all required variables.
|
||||
- Migrations create the planned schema.
|
||||
- The Node `.mjs` migration script can run pure SQL files against PostgreSQL.
|
||||
- The app still passes lint/build.
|
||||
- No future milestone features were implemented.
|
||||
@@ -0,0 +1,82 @@
|
||||
# Milestone 02 — Google Auth and Admin Shell
|
||||
|
||||
## Reference
|
||||
|
||||
Read `plans/wedding-bootstrap-plan.md` first. Also confirm the current Next.js/Auth.js setup before implementation because this project uses Next.js 16.
|
||||
|
||||
## Goal
|
||||
|
||||
Add Google login restricted to the couple and create a protected `/admin` shell using shadcn/ui components only.
|
||||
|
||||
## Scope
|
||||
|
||||
Implement only:
|
||||
|
||||
- Google OAuth authentication.
|
||||
- Admin email allowlist via `ADMIN_EMAILS`.
|
||||
- Route protection for `/admin` and `/admin/*`.
|
||||
- Basic `/admin` dashboard shell.
|
||||
- Sign-in/sign-out UI.
|
||||
- shadcn/ui setup and required admin shell components.
|
||||
|
||||
## Non-goals
|
||||
|
||||
Do not implement:
|
||||
|
||||
- Invitation creation forms.
|
||||
- Invitation list/detail functionality.
|
||||
- Guest invite pages.
|
||||
- RSVP submission.
|
||||
- MinIO uploads.
|
||||
- Song/toast/message workflows.
|
||||
|
||||
## Required constraints
|
||||
|
||||
- `/admin` and `/admin/*` require authenticated admin access.
|
||||
- Only emails in `ADMIN_EMAILS` can access admin pages.
|
||||
- Guest pages must not require login.
|
||||
- Admin UI must use shadcn/ui components only.
|
||||
- Do not introduce another admin component library.
|
||||
- If a component is needed, add it through the shadcn workflow.
|
||||
|
||||
## Implementation tasks
|
||||
|
||||
- Add/configure Auth.js/NextAuth Google provider.
|
||||
- Add `auth.ts` or equivalent auth config.
|
||||
- Add `app/api/auth/[...nextauth]/route.ts` or current-version equivalent.
|
||||
- Add middleware or route-level protection for `/admin`.
|
||||
- Add forbidden/unauthorized handling.
|
||||
- Initialize shadcn/ui if not already initialized.
|
||||
- Add required shadcn components for the admin shell, likely:
|
||||
- button
|
||||
- card
|
||||
- dropdown-menu
|
||||
- avatar or badge if useful
|
||||
- alert
|
||||
- Create `/admin` page with basic dashboard placeholder cards.
|
||||
- Add sign-in/sign-out controls.
|
||||
|
||||
## Verification
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
Manual checks:
|
||||
|
||||
- Unauthenticated user cannot access `/admin`.
|
||||
- Allowed Google email can access `/admin`.
|
||||
- Non-allowlisted Google email is denied.
|
||||
- Public/guest routes remain accessible without login.
|
||||
- Admin page uses only shadcn/ui components.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Admin auth works with Google OAuth and email allowlist.
|
||||
- `/admin` is protected.
|
||||
- Admin shell exists and builds.
|
||||
- No non-shadcn admin UI component library was added.
|
||||
- No future milestone features were implemented.
|
||||
@@ -0,0 +1,106 @@
|
||||
# Milestone 03 — Admin Invitation Management
|
||||
|
||||
## Reference
|
||||
|
||||
Read `plans/wedding-bootstrap-plan.md` first. This milestone depends on Milestone 01 and Milestone 02 being complete.
|
||||
|
||||
## Goal
|
||||
|
||||
Allow admins to create, list, and view invitations and invitation members from the protected admin portal.
|
||||
|
||||
## Scope
|
||||
|
||||
Implement only:
|
||||
|
||||
- Invitation query modules.
|
||||
- Invitation member query modules.
|
||||
- Admin invitation list page.
|
||||
- Admin create invitation page.
|
||||
- Admin invitation detail page.
|
||||
- Required shadcn/ui admin components.
|
||||
- Copy invite link affordance.
|
||||
- Language selection per invitation.
|
||||
|
||||
## Non-goals
|
||||
|
||||
Do not implement:
|
||||
|
||||
- Guest RSVP submission.
|
||||
- Guest-facing localized page.
|
||||
- MinIO uploads.
|
||||
- Song/toast/message guest forms.
|
||||
- CSV export unless trivial and explicitly requested later.
|
||||
- Final design polish.
|
||||
|
||||
## Required constraints
|
||||
|
||||
- Admin UI must use shadcn/ui components only.
|
||||
- The admin create/edit form must require `language`.
|
||||
- Use the invitation/member model from the main plan.
|
||||
- Generate unguessable invitation tokens.
|
||||
- Use Kysely query modules; do not add an ORM.
|
||||
- Keep routes protected by admin auth.
|
||||
|
||||
## Implementation tasks
|
||||
|
||||
- Add query helpers for:
|
||||
- create invitation with members
|
||||
- list invitations with derived counts/status
|
||||
- get invitation detail by id
|
||||
- update invitation metadata if included
|
||||
- add/update/remove members if included
|
||||
- Add validation schema for invitation creation.
|
||||
- Add supported language config if not already present.
|
||||
- Add `/admin/guests` page.
|
||||
- Add `/admin/guests/new` page.
|
||||
- Add `/admin/guests/[id]` page.
|
||||
- Use shadcn components for:
|
||||
- table
|
||||
- form
|
||||
- input
|
||||
- select
|
||||
- checkbox
|
||||
- button
|
||||
- card
|
||||
- badge
|
||||
- dropdown/action menu if needed
|
||||
- alert/toast if needed
|
||||
|
||||
## Admin fields
|
||||
|
||||
Create invitation form must include:
|
||||
|
||||
- display name
|
||||
- language
|
||||
- contact email
|
||||
- contact phone
|
||||
- max party size
|
||||
- allow member edits
|
||||
- notes
|
||||
- initial members
|
||||
|
||||
## Verification
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
Manual checks:
|
||||
|
||||
- Admin can create an invitation with multiple members.
|
||||
- Language is required and saved.
|
||||
- Invitation token is generated.
|
||||
- Invite link can be copied or displayed.
|
||||
- Invitation appears in list.
|
||||
- Invitation detail page displays members and metadata.
|
||||
- Non-admin cannot access these pages.
|
||||
- Admin pages use shadcn/ui only.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Admin can manage the initial invitation/member data needed for guest RSVP.
|
||||
- Invitation language exists in the UI and database.
|
||||
- The implementation does not include guest RSVP or uploads yet.
|
||||
@@ -0,0 +1,97 @@
|
||||
# Milestone 04 — Guest RSVP and Invitation Language
|
||||
|
||||
## Reference
|
||||
|
||||
Read `plans/wedding-bootstrap-plan.md` first. This milestone depends on Milestones 01 and 03. Admin auth from Milestone 02 should already protect admin pages.
|
||||
|
||||
## Goal
|
||||
|
||||
Build the guest-facing `/invite/[token]` flow with localized copy based on `invitations.language`, per-member attendance, per-member meal/dietary fields, group logistics, and deadline behavior.
|
||||
|
||||
## Scope
|
||||
|
||||
Implement only:
|
||||
|
||||
- `/invite/[token]` page.
|
||||
- Invalid invitation handling.
|
||||
- Localization dictionary foundation for guest-facing copy.
|
||||
- Per-member RSVP form.
|
||||
- Group logistics form.
|
||||
- Save/update `invitation_members` attendance/meal/dietary fields.
|
||||
- Save/update `invitation_responses` logistics and `submitted_at`.
|
||||
- RSVP deadline read-only behavior.
|
||||
- Admin visibility of submitted RSVP data if not already present.
|
||||
|
||||
## Non-goals
|
||||
|
||||
Do not implement:
|
||||
|
||||
- MinIO photo upload.
|
||||
- Song requests.
|
||||
- Toast requests.
|
||||
- Guest messages.
|
||||
- Advanced final visual design.
|
||||
- New admin component libraries.
|
||||
|
||||
## Required constraints
|
||||
|
||||
- `invitations.language` is the source of truth for guest-facing language.
|
||||
- Do not rely only on browser language detection.
|
||||
- `invitation_members.attending` is the source of truth for attendance.
|
||||
- Do not store duplicate attendance status on `invitation_responses`.
|
||||
- Edits are allowed until `RSVP_DEADLINE`.
|
||||
- After the deadline, guest RSVP data is read-only.
|
||||
- Guest-facing design may be custom; shadcn-only restriction applies to admin pages.
|
||||
|
||||
## Implementation tasks
|
||||
|
||||
- Add `lib/i18n/config.ts` with supported languages.
|
||||
- Add translation dictionaries under `lib/i18n/dictionaries/`.
|
||||
- Add helper to resolve dictionary by invitation language.
|
||||
- Add response/member validation schemas.
|
||||
- Add query helpers for:
|
||||
- find invitation by token with members/response
|
||||
- update member RSVP fields
|
||||
- upsert invitation response
|
||||
- Implement `/invite/[token]`.
|
||||
- Add form handling using current Next.js best practice.
|
||||
- Add deadline helper.
|
||||
- Ensure admin detail/list can show derived RSVP status and submitted response data.
|
||||
|
||||
## Localized guest copy must include
|
||||
|
||||
- Page headings.
|
||||
- RSVP labels.
|
||||
- Meal/dietary labels.
|
||||
- Travel/logistics labels.
|
||||
- Validation messages.
|
||||
- Submit/success/error messages.
|
||||
- Deadline/read-only messages.
|
||||
|
||||
## Verification
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
Manual checks:
|
||||
|
||||
- Invalid token shows a friendly invalid invitation page.
|
||||
- Valid token shows personalized invitation.
|
||||
- Page renders using the invitation's configured language.
|
||||
- Different members can have different attendance values.
|
||||
- Different members can have different meal preferences/dietary restrictions.
|
||||
- Group logistics save correctly.
|
||||
- `submitted_at` is set after submission.
|
||||
- Admin can see submitted RSVP data.
|
||||
- After deadline, guest form is read-only.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Guest RSVP MVP works end-to-end.
|
||||
- Localization is driven by invitation language.
|
||||
- Attendance is stored only per member.
|
||||
- No upload or optional interaction features were implemented yet.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Milestone 05 — Optional Guest Interactions
|
||||
|
||||
## Reference
|
||||
|
||||
Read `plans/wedding-bootstrap-plan.md` first. This milestone depends on Milestone 04.
|
||||
|
||||
## Goal
|
||||
|
||||
Add optional guest interaction sections that make the wedding experience more personal: song requests, toast/speech requests, and advice/wish/memory messages.
|
||||
|
||||
## Scope
|
||||
|
||||
Implement only:
|
||||
|
||||
- Guest song request form section.
|
||||
- Guest toast/speech request form section.
|
||||
- Guest advice/wish/memory message form section.
|
||||
- Query modules for these tables.
|
||||
- Validation schemas.
|
||||
- Admin display and basic management for these submissions.
|
||||
- Localization copy for these sections.
|
||||
|
||||
## Non-goals
|
||||
|
||||
Do not implement:
|
||||
|
||||
- MinIO photo upload.
|
||||
- Presigned URLs.
|
||||
- CSV export unless explicitly requested later.
|
||||
- Complex scheduling UI for toasts.
|
||||
- Final design polish.
|
||||
|
||||
## Required constraints
|
||||
|
||||
- Guest-facing copy must use `invitations.language`.
|
||||
- Admin UI must use shadcn/ui components only.
|
||||
- Use Kysely query modules; do not add an ORM.
|
||||
- Song requests should be limited, recommended max: 3 per invitation.
|
||||
- Toast requests are voluntary and admin-approved.
|
||||
|
||||
## Implementation tasks
|
||||
|
||||
- Add query helpers for:
|
||||
- song requests
|
||||
- toast requests
|
||||
- guest messages
|
||||
- Add Zod schemas for interactions.
|
||||
- Add localized copy for interaction sections.
|
||||
- Add guest-facing form sections to `/invite/[token]`.
|
||||
- Persist submissions.
|
||||
- Add admin views on invitation detail page for:
|
||||
- song requests
|
||||
- toast requests
|
||||
- guest messages
|
||||
- Add toast status controls if feasible:
|
||||
- pending
|
||||
- accepted
|
||||
- declined
|
||||
- scheduled
|
||||
|
||||
## Verification
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
Manual checks:
|
||||
|
||||
- Guest can submit up to 3 song requests.
|
||||
- Guest can submit optional toast/speech request.
|
||||
- Guest can submit advice/wish/memory message.
|
||||
- Guest-facing text is localized by invitation language.
|
||||
- Admin can view all submitted interaction data.
|
||||
- Admin can update toast request status if implemented.
|
||||
- Admin pages use shadcn/ui only.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- Optional guest interactions work end-to-end.
|
||||
- Admin can review them.
|
||||
- No upload functionality was implemented in this milestone.
|
||||
@@ -0,0 +1,100 @@
|
||||
# Milestone 06 — MinIO Photo Uploads
|
||||
|
||||
## Reference
|
||||
|
||||
Read `plans/wedding-bootstrap-plan.md` first. This milestone depends on Milestones 01, 03, and 04.
|
||||
|
||||
## Goal
|
||||
|
||||
Add one-photo-per-invitation upload support using MinIO through S3-compatible presigned URLs.
|
||||
|
||||
## Scope
|
||||
|
||||
Implement only:
|
||||
|
||||
- S3/MinIO client.
|
||||
- Presigned upload endpoint.
|
||||
- Upload completion endpoint.
|
||||
- Guest-facing photo upload UI.
|
||||
- One-current-photo-per-invitation behavior.
|
||||
- Replacement until deadline.
|
||||
- Admin signed preview/download URL.
|
||||
- Upload validation.
|
||||
|
||||
## Non-goals
|
||||
|
||||
Do not implement:
|
||||
|
||||
- Multiple photos per invitation.
|
||||
- Public unauthenticated photo browsing.
|
||||
- Image processing/resizing unless explicitly requested later.
|
||||
- Complex gallery UI.
|
||||
- Another storage provider.
|
||||
|
||||
## Required constraints
|
||||
|
||||
- Use MinIO via S3-compatible AWS SDK APIs.
|
||||
- One image per invitation/group.
|
||||
- Replacing the image is allowed until `RSVP_DEADLINE`.
|
||||
- After the deadline, guest upload is read-only/disabled.
|
||||
- Validate invitation token before presigning upload.
|
||||
- Admin UI must use shadcn/ui components only.
|
||||
- Guest-facing copy must use `invitations.language`.
|
||||
|
||||
## Implementation tasks
|
||||
|
||||
- Add `lib/storage/s3.ts`.
|
||||
- Add upload helper functions in `lib/storage/uploads.ts`.
|
||||
- Add validation schema for presign/complete requests.
|
||||
- Add `app/api/uploads/presign/route.ts`.
|
||||
- Add `app/api/uploads/complete/route.ts`.
|
||||
- Generate object keys like:
|
||||
|
||||
```txt
|
||||
invitations/{invitationId}/{uuid}-{safeFilename}
|
||||
```
|
||||
|
||||
- Upsert `photo_uploads` metadata.
|
||||
- Optionally delete old object when replacing.
|
||||
- Add guest upload component/section.
|
||||
- Add localized upload copy.
|
||||
- Add admin preview/download via short-lived signed GET URL.
|
||||
|
||||
## Validation rules
|
||||
|
||||
- Allowed MIME types:
|
||||
- `image/jpeg`
|
||||
- `image/png`
|
||||
- `image/webp`
|
||||
- Enforce a maximum file size.
|
||||
- Require valid invitation token.
|
||||
- Require deadline not passed for guest upload/replace.
|
||||
|
||||
## Verification
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
docker compose up -d postgres minio minio-init
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
Manual checks:
|
||||
|
||||
- Guest can upload one valid image.
|
||||
- Admin can preview/download the image.
|
||||
- Guest can replace image before deadline.
|
||||
- Metadata in `photo_uploads` is updated on replace.
|
||||
- Invalid MIME type is rejected.
|
||||
- Oversized file is rejected.
|
||||
- Invalid token cannot presign upload.
|
||||
- After deadline, upload/replace is disabled.
|
||||
- Admin pages use shadcn/ui only.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- MinIO upload flow works end-to-end.
|
||||
- Exactly one current photo is associated with each invitation.
|
||||
- Uploads are secured by invitation token and deadline.
|
||||
- Admin can access uploaded photo previews/downloads.
|
||||
@@ -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