Feature: Membership Quests System + Email Consent Quest #474

Open
opened 2026-04-16 06:56:39 +00:00 by jjalangtry · 0 comments
jjalangtry commented 2026-04-16 06:56:39 +00:00 (Migrated from github.com)

Problem

The existing Profile Quest (lines 1164–1242 in ProfileContent.tsx) is fully hardcoded — the quest card, the field checklist, the completion logic, and the CTA button are all one-off JSX baked into the profile page. There is no way to add a second quest without copy-pasting the whole block and manually wiring up new logic.

We need:

  1. A quest component/system that renders any quest from a shared definition — so adding Quest #2, #3, etc. is just adding one entry to a config
  2. Quest #2: Email Communications Consent — a new quest that lets members opt into email categories (Elections, Amendments, Events, General)

What Exists Today

File: next/app/(main)/profile/[id]/ProfileContent.tsx

// Lines 244–253 — the current quest definition
const QUEST_FIELDS = [
  { key: "graduationTerm", label: "Graduation Term", icon: GraduationCap },
  { key: "graduationYear", label: "Graduation Year", icon: Calendar },
  { key: "major",          label: "Major",           icon: Briefcase },
  { key: "gitHub",         label: "GitHub",           icon: Github },
  { key: "linkedIn",       label: "LinkedIn",         icon: Linkedin },
] as const;

The completion check (fieldComplete), progress bar, checklist, and "Complete Quest" button are all inline JSX — lines 941–1242. There is no DB backing; it reads directly from the user's profile fields.


Proposed Solution

Step 1 — Extract a reusable <QuestCard /> component

Pull the existing quest card out of ProfileContent.tsx into its own component so both quests share the same visual shell (gradient card, Target icon, progress bar, checklist, CTA button).

New file: next/components/quests/QuestCard.tsx

The component accepts:

  • title — quest name
  • description — one-liner shown under the title
  • items — array of { label, complete, icon } (already computed by caller)
  • ctaLabel — button text
  • onCta — button click handler

This keeps quest-specific logic (what counts as complete, what the CTA does) in each quest's own component while sharing the shell.


Step 2 — Keep Profile Quest as-is, just using the new shell

Refactor the existing profile quest to use <QuestCard />. Behavior does not change — it still reads profile fields, still opens edit mode on "Complete Quest".


New file: next/components/quests/EmailConsentQuest.tsx

This quest shows below the Profile Quest on the member's own profile page. It:

  • Shows four checkboxes: Elections, Amendments, Events, General Announcements (+ an implicit "None — don't email me" if nothing is checked)
  • Saves selections to a new EmailPreferences record in the DB
  • Marks itself "complete" once the user has submitted at least once (even if they chose none)
  • Can be re-opened to update preferences

Prisma model addition:

model EmailPreferences {
  id         String   @id @default(cuid())
  userId     String   @unique
  user       User     @relation(fields: [userId], references: [id], onDelete: Cascade)

  elections  Boolean  @default(false)
  amendments Boolean  @default(false)
  events     Boolean  @default(false)
  general    Boolean  @default(false)

  updatedAt  DateTime @updatedAt
}

New API routes:

Method Route Purpose
GET /api/email-preferences Return current user's preferences (null if never set)
PUT /api/email-preferences Upsert preferences for current user

Completion state: EmailPreferences row exists for this user → quest is complete. Shown as incomplete until first save.


Step 4 — Render both quests on the profile page

In ProfileContent.tsx, replace the inline quest block with:

{/* Quests — only visible to profile owner */}
{profile.isOwner && (
  <div className="flex flex-col gap-3">
    {!allQuestComplete && <ProfileQuestCard profile={profile} onEdit={enterEditMode} />}
    <EmailConsentQuestCard userId={profile.id} />
  </div>
)}

Adding a future quest: create a new XyzQuestCard.tsx using <QuestCard />, add its API route, drop it into the list above. Nothing else changes.


Scope

In scope:

  • Extract <QuestCard /> shared shell component
  • Refactor existing profile quest to use it (no behavior change)
  • EmailPreferences Prisma model + migration
  • GET /PUT /api/email-preferences API routes
  • EmailConsentQuestCard component with four checkboxes and save
  • Render both quests on profile page

Out of scope:

  • Database-driven quest definitions (config file is sufficient)
  • Gamification / points for quest completion
  • Admin UI to manage quests

Acceptance Criteria

  • The existing Profile Quest looks and behaves identically to today after the refactor
  • A new "Email Communications" quest card appears below it on the member's own profile page
  • The email quest shows checkboxes for Elections, Amendments, Events, General Announcements
  • Saving (even with nothing checked) marks the quest complete and collapses the card
  • The member can re-expand the card to change their selections at any time
  • Selections are persisted in EmailPreferences and returned by GET /api/email-preferences
  • Adding a third quest in the future requires only: a new QuestCard-based component + adding it to the profile page list
  • Unauthenticated requests to /api/email-preferences return 401
  • A member cannot update another member's preferences (403)

Additional Context

  • Epic: #472
  • Email consent feeds the broadcasting system — issue #473 queries EmailPreferences to filter recipients
  • Existing quest UI: next/app/(main)/profile/[id]/ProfileContent.tsx lines 244–253 and 1164–1242
  • Existing email lib: next/lib/email/index.ts
## Problem The existing **Profile Quest** (lines 1164–1242 in `ProfileContent.tsx`) is fully hardcoded — the quest card, the field checklist, the completion logic, and the CTA button are all one-off JSX baked into the profile page. There is no way to add a second quest without copy-pasting the whole block and manually wiring up new logic. We need: 1. A quest component/system that renders any quest from a shared definition — so adding Quest #2, #3, etc. is just adding one entry to a config 2. **Quest #2: Email Communications Consent** — a new quest that lets members opt into email categories (Elections, Amendments, Events, General) --- ## What Exists Today **File:** `next/app/(main)/profile/[id]/ProfileContent.tsx` ```ts // Lines 244–253 — the current quest definition const QUEST_FIELDS = [ { key: "graduationTerm", label: "Graduation Term", icon: GraduationCap }, { key: "graduationYear", label: "Graduation Year", icon: Calendar }, { key: "major", label: "Major", icon: Briefcase }, { key: "gitHub", label: "GitHub", icon: Github }, { key: "linkedIn", label: "LinkedIn", icon: Linkedin }, ] as const; ``` The completion check (`fieldComplete`), progress bar, checklist, and "Complete Quest" button are all inline JSX — lines 941–1242. There is no DB backing; it reads directly from the user's profile fields. --- ## Proposed Solution ### Step 1 — Extract a reusable `<QuestCard />` component Pull the existing quest card out of `ProfileContent.tsx` into its own component so both quests share the same visual shell (gradient card, Target icon, progress bar, checklist, CTA button). **New file:** `next/components/quests/QuestCard.tsx` The component accepts: - `title` — quest name - `description` — one-liner shown under the title - `items` — array of `{ label, complete, icon }` (already computed by caller) - `ctaLabel` — button text - `onCta` — button click handler This keeps quest-specific logic (what counts as complete, what the CTA does) in each quest's own component while sharing the shell. --- ### Step 2 — Keep Profile Quest as-is, just using the new shell Refactor the existing profile quest to use `<QuestCard />`. Behavior does not change — it still reads profile fields, still opens edit mode on "Complete Quest". --- ### Step 3 — Add Quest #2: Email Consent **New file:** `next/components/quests/EmailConsentQuest.tsx` This quest shows below the Profile Quest on the member's own profile page. It: - Shows four checkboxes: **Elections**, **Amendments**, **Events**, **General Announcements** (+ an implicit "None — don't email me" if nothing is checked) - Saves selections to a new `EmailPreferences` record in the DB - Marks itself "complete" once the user has submitted at least once (even if they chose none) - Can be re-opened to update preferences **Prisma model addition:** ```prisma model EmailPreferences { id String @id @default(cuid()) userId String @unique user User @relation(fields: [userId], references: [id], onDelete: Cascade) elections Boolean @default(false) amendments Boolean @default(false) events Boolean @default(false) general Boolean @default(false) updatedAt DateTime @updatedAt } ``` **New API routes:** | Method | Route | Purpose | |--------|-------|---------| | `GET` | `/api/email-preferences` | Return current user's preferences (null if never set) | | `PUT` | `/api/email-preferences` | Upsert preferences for current user | Completion state: `EmailPreferences` row exists for this user → quest is complete. Shown as incomplete until first save. --- ### Step 4 — Render both quests on the profile page In `ProfileContent.tsx`, replace the inline quest block with: ```tsx {/* Quests — only visible to profile owner */} {profile.isOwner && ( <div className="flex flex-col gap-3"> {!allQuestComplete && <ProfileQuestCard profile={profile} onEdit={enterEditMode} />} <EmailConsentQuestCard userId={profile.id} /> </div> )} ``` **Adding a future quest:** create a new `XyzQuestCard.tsx` using `<QuestCard />`, add its API route, drop it into the list above. Nothing else changes. --- ## Scope **In scope:** - Extract `<QuestCard />` shared shell component - Refactor existing profile quest to use it (no behavior change) - `EmailPreferences` Prisma model + migration - `GET /PUT /api/email-preferences` API routes - `EmailConsentQuestCard` component with four checkboxes and save - Render both quests on profile page **Out of scope:** - Database-driven quest definitions (config file is sufficient) - Gamification / points for quest completion - Admin UI to manage quests --- ## Acceptance Criteria - [ ] The existing Profile Quest looks and behaves identically to today after the refactor - [ ] A new "Email Communications" quest card appears below it on the member's own profile page - [ ] The email quest shows checkboxes for Elections, Amendments, Events, General Announcements - [ ] Saving (even with nothing checked) marks the quest complete and collapses the card - [ ] The member can re-expand the card to change their selections at any time - [ ] Selections are persisted in `EmailPreferences` and returned by `GET /api/email-preferences` - [ ] Adding a third quest in the future requires only: a new `QuestCard`-based component + adding it to the profile page list - [ ] Unauthenticated requests to `/api/email-preferences` return 401 - [ ] A member cannot update another member's preferences (403) ## Additional Context - Epic: #472 - Email consent feeds the broadcasting system — issue #473 queries `EmailPreferences` to filter recipients - Existing quest UI: `next/app/(main)/profile/[id]/ProfileContent.tsx` lines 244–253 and 1164–1242 - Existing email lib: `next/lib/email/index.ts`
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
SSE/WebsiteTheSSEquel#474
No description provided.