Skip to content

feat: restyle user info page to match Figma (#151) - #155

Merged
joengy merged 7 commits into
mainfrom
feature/user-info-page-151
Oct 11, 2026
Merged

joengy merged 7 commits into
mainfrom
feature/user-info-page-151

Conversation

@Richman-Tan

@Richman-Tan Richman-Tan commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Restyles /profile to match the user page design in Figma (node 5457-12171). Layout, data and behaviour are unchanged — this is a visual pass over the existing page.

  • Header — name in ssa-red bold with the email beneath, and the two controls from the mock on the right: an outlined pencil circle and a LOG OUT pill.
  • New ProfileCard — cream fill (ssa-card), muted-taupe heading, no rule, and a 2px shadow that barely lifts it off the background. Deliberately not the shared CardSection: that one is the ssa-yellow / Averia / red-rule treatment and has five other callers (signup steps, sign-in, PaymentStep), so changing it would have redesigned those pages too.
  • Fields — DM Mono uppercase labels stacked over ssa-grey values, reusing the caption idiom already on the events cards. Two per row, collapsing to one below sm.
  • Status — full-width pill with a leading dot rather than an inline chip, matching the mock.
  • Container — max-w-[756px], with gap-8 (32px) between the header row and the card stack and gap-6 (24px) between the cards. Measured off the 1440px artboard, where the column is a fixed 756px centred at left 342 (342 + 756 + 342 = 1440). An earlier revision of this PR estimated the column as a percentage of a 1280px frame and landed on max-w-3xl / 768px; 7441090 replaced that estimate with the direct measurement, so 756px is deliberate and 768px is the stale number.

Notes for review

Two places where I did not follow the mock exactly, both flagged deliberately:

  1. Label colour. The mock uses #8c8880 for labels and placeholders. At 11px that only reaches 3.3:1 on the cream card, below WCAG AA's 4.5:1 for small text, so labels use #706f6f (ssa-badge-light-text, 4.8:1) instead. Card headings do use #8c8880 — at 24px bold they qualify as large text, where 3:1 passes. Happy to revert if design prefers exact fidelity.
  2. Delete account. The mock's header only carries the pencil and LOG OUT, but DELETE /api/auth/me is live and the header was its only entry point. Rather than drop a working destructive action, it moved to a quiet underlined link at the bottom of the page. Worth a design decision — if it should be gone entirely, say so and I'll remove it.

Also fixes a latent bug in StatusBadge: (status && MAP[status]) ?? fallback returns '' when membershipStatus is an empty string, because ?? only catches null/undefined — so the badge rendered blank instead of "Unknown".

Review follow-ups

  • Header controls now invert their fill on hover (outline fills solid, icon/label flips to white) instead of recolouring the outline red, per Robin's note. 266e0cd
  • ViewField chose its placeholder with ??, which only catches null/undefined — but userToForm normalises missing session fields to '', so First Name, Last Name, Phone, UPI, Student ID and Area of Study rendered a blank line instead of "Not provided". Switched to ||. 266e0cd
  • Edit-mode inputs, selects and the returning-member switch had a visible caption but no accessible name. FieldLabel now renders a real <label> when given htmlFor; the switch is referenced with aria-labelledby. dad2668
  • Contrast: four AA failures Copilot flagged all checked out, and the same white-on-ssa-red pair failed in three more places. Status pill labels now use #633c3c with the dot carrying the status hue; a new --color-ssa-red-dark (#c8324c, 5.23:1 with white) backs the dialog confirm, sticky save bar, error toast and delete-link hover; the email and LOG OUT label move to #706f6f. Every text pair on the page now clears 4.5:1 and every non-text pair 3:1 — full table in 1801d98. This adds one palette token and moves the pills off the mock's exact colours, so it wants a design sign-off. 1801d98

Type of change

  • Feature
  • Bug fix
  • Chore / config
  • Hotfix

Checklist

  • My branch follows the naming convention (feature/, fix/, chore/, hotfix/)
  • My commit messages follow the conventional commits format
  • CI checks pass

Verification

Ran the page against a locally minted session (tsc --noEmit, eslint, next build all clean) and checked it in the browser: read mode matches the mock, edit mode still renders 6 inputs / 3 selects / 1 toggle with the sticky save bar, and cancel restores the read view. Computed styles confirmed against the mock — card #fff8ec, 12px radius, 36px card padding at sm and up (24px below), 32px header-to-stack gap, 24px gap between cards, DM Mono 11px labels, 756px column.

Merging

Please use Squash and merge, and clear the prefilled commit-message body before confirming — this repo prefills it with the concatenated branch commit messages, and those carry trailers that should not land on main.

Linked Issues

Closes #151

Copilot AI lite review requested due to automatic review settings August 13, 2026 14:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Restyles the /profile page to match the Figma user page design while keeping existing layout/data/behavior, and introduces a dedicated ProfileCard to avoid changing the shared CardSection styling used elsewhere.

Changes:

  • Adjusted /profile page container sizing/spacing to max-w-3xl with updated responsive padding.
  • Reworked ProfileClient presentation: new field label/value styling, updated header controls, and redesigned membership StatusBadge (including an empty-string fallback fix).
  • Added a new ProfileCard component implementing the quieter cream/taupe card treatment for the profile page.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
web/src/app/profile/page.tsx Updates page container width/padding to match Figma spacing.
web/src/app/profile/_components/ProfileClient.tsx Restyles header/cards/fields and updates StatusBadge rendering logic.
web/src/app/profile/_components/ProfileCard.tsx Adds a profile-specific card component to avoid impacting shared CardSection callers.
Suppressed comments (3)

web/src/app/profile/_components/ProfileClient.tsx:127

  • EditText renders a visual label but the <input> has no programmatic accessible name (the label is a <span> not associated with the control). Add an aria-label (or convert to a real <label> association) so screen readers can announce the field name.

This issue also appears in the following locations of the same file:

  • line 144
  • line 190
    <div className="flex flex-col gap-2">
      <FieldLabel>{label}</FieldLabel>
      <input
        type={type}
        value={value}
        onChange={(e) => onChange(e.target.value)}
        className={inputCls}
      />

web/src/app/profile/_components/ProfileClient.tsx:151

  • EditSelect renders a visual label but the <select> has no programmatic accessible name. Add an aria-label (or convert the label to a proper <label>) so the control is announced correctly by assistive tech.
    <div className="flex flex-col gap-2">
      <FieldLabel>{label}</FieldLabel>
      <div className="relative">
        <select
          value={value}
          onChange={(e) => onChange(e.target.value)}
          className={`${inputCls} appearance-none pr-8 ${value ? 'text-ssa-grey' : 'text-ssa-badge-light-text'}`}
        >

web/src/app/profile/_components/ProfileClient.tsx:199

  • EditToggle has role="switch" but no accessible name, so screen readers will announce an unlabeled switch. Add aria-label={label} (or aria-labelledby) so the switch is identifiable.
    <div className="flex flex-col gap-2">
      <FieldLabel>{label}</FieldLabel>
      <button
        type="button"
        role="switch"
        aria-checked={value}
        onClick={() => onChange(!value)}
        className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-ssa-red focus:ring-offset-1 ${
          value ? 'bg-ssa-red' : 'bg-ssa-muted-taupe/40'
        }`}

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread web/src/app/profile/_components/ProfileClient.tsx
Copilot AI review requested due to automatic review settings August 13, 2026 14:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (4)

web/src/app/profile/_components/ProfileClient.tsx:127

  • EditText renders a visible label but the <input> has no programmatic label (no <label htmlFor>, aria-label, or aria-labelledby). This makes the control harder to use with screen readers. Adding aria-label={label} here is a minimal fix.

This issue also appears in the following locations of the same file:

  • line 148
  • line 193
        type={type}
        value={value}
        onChange={(e) => onChange(e.target.value)}
        className={inputCls}
      />

web/src/app/profile/_components/ProfileClient.tsx:197

  • EditToggle uses role="switch" but doesn't provide an accessible name (no aria-label / aria-labelledby). Screen readers will announce it as an unnamed switch. Adding aria-label={label} is a minimal fix.
        type="button"
        role="switch"
        aria-checked={value}
        onClick={() => onChange(!value)}
        className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-ssa-red focus:ring-offset-1 ${

web/src/app/profile/_components/ProfileClient.tsx:151

  • EditSelect renders a visible label but the <select> has no programmatic label (no <label htmlFor>, aria-label, or aria-labelledby). Adding aria-label={label} is a minimal, self-contained fix.
          value={value}
          onChange={(e) => onChange(e.target.value)}
          className={`${inputCls} appearance-none pr-8 ${value ? 'text-ssa-grey' : 'text-ssa-badge-light-text'}`}
        >

web/src/app/profile/page.tsx:13

  • The PR description says the profile container should be max-w-3xl (768px), but the implementation hard-codes max-w-[756px] (and the inline comment also describes 756px). Please align either the code to the described max-w-3xl, or update the PR description/design notes so reviewers know 756px is intentional.
  // Figma (1440px artboard): the column is a fixed 756px centred at left 342, and
  // the card stack (Frame 845) sits on a 24px gap.
  return (
    <main className="min-h-screen bg-ssa-background px-4 pb-24 pt-[88px] sm:px-6">
      <div className="mx-auto mt-8 flex w-full max-w-[756px] flex-col gap-6">

Copilot AI review requested due to automatic review settings August 13, 2026 14:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

joengy
joengy previously approved these changes Aug 14, 2026

@joengy joengy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user profile page looks great, thank you for implementing it so fast.

@Robin-deng617
Robin-deng617 requested review from Robin-deng617 and removed request for Robin-deng617 August 19, 2026 08:59

@Robin-deng617 Robin-deng617 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks 1 on 1 with the figma. never dobuted you richman. just had 1 question though.

Comment thread web/src/app/profile/_components/ProfileClient.tsx
Header controls (pencil, LOG OUT) now invert their fill on hover — the
outline fills solid and the icon/label flips to white — instead of
recolouring the outline red. The red-outline hover they had was not a
deliberate nod to the "Delete account" link; it just did not match the
mock. The delete link keeps its own quiet red hover, since it is not in
the mock at all.

ViewField also chose its placeholder with `??`, which only catches
null/undefined. userToForm normalises every missing session field to `''`,
so First Name, Last Name, Phone, UPI, Student ID and Area of Study
rendered a blank line instead of "Not provided" when absent from the
session. Switched to `||`, matching the StatusBadge fix already in this PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Resolve the documented card-padding and desktop max-width discrepancies.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread web/src/app/profile/_components/ProfileCard.tsx
Comment thread web/src/app/profile/page.tsx
FieldLabel rendered a <span>, so the inputs, selects and the returning-member
switch in edit mode had a visible caption but no accessible name — screen
readers announced them as unlabelled.

FieldLabel now renders a real <label> when given `htmlFor`, so clicking the
caption also focuses the field. ViewField has nothing to point at and keeps
the <span>; the toggle is a button, which htmlFor cannot target, so it takes
an id and is referenced with aria-labelledby.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 15, 2026 03:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Address insufficient contrast in status pills and the delete confirmation control.

Review details

Suppressed comments (4)

Previously missed (1) — in code that hasn't changed since the last review.

web/src/app/profile/_components/ProfileClient.tsx:254

  • The expired and pending variants put normal 14px text on #ffccd5 and #ffe3ac; #f85b76 and #9f7d32 yield only about 2.1:1 and 3.0:1 contrast, respectively, below WCAG AA. Please use a darker text token for the pill text while retaining the red/gold dot colors.

This issue also appears in the following locations of the same file:

  • line 298
  • line 576

web/src/app/profile/_components/ProfileClient.tsx:576

  • This 16px email uses #8c8880 (ssa-muted-taupe) on the #fffbf4 page background, which is only about 3.4:1 and does not meet the 4.5:1 AA threshold for normal text. Since the PR already darkens small labels for contrast, use an accessible text token here as well.
          <p className="truncate font-inter text-base leading-6 tracking-[-0.4px] text-ssa-muted-taupe">

web/src/app/profile/_components/ProfileClient.tsx:299

  • The logout label is also 16px normal-sized text, but headerControlIdle renders it as #8c8880 on the light page background (about 3.4:1) and white on #8c8880 on hover (about 3.5:1), both below AA. Please darken the idle text/background token so both states remain readable.
const headerControlIdle =
  'border-ssa-muted-taupe text-ssa-muted-taupe hover:bg-ssa-muted-taupe hover:text-ssa-white'

web/src/app/profile/_components/ProfileClient.tsx:400

  • This restyle changes the delete confirmation's former bg-red-600 to bg-ssa-red while keeping a 14px white label. White on #f85b76 is only about 3.1:1, below AA for normal text; retain a darker destructive background for the delete variant (or use a dark label).
            className="flex w-full items-center justify-center gap-2 rounded-full bg-ssa-red py-2.5 font-be-vietnam-pro text-sm font-semibold text-ssa-white transition-opacity hover:opacity-90 disabled:opacity-60"
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot flagged four contrast failures; all four check out, and the same
white-on-ssa-red pair failed in three more places in the file.

Status pills: the mock's own colours put #f85b76 on #ffccd5 (2.20:1) and
#9f7d32 on #ffe3ac (3.09:1) behind 14px text. The dot now carries the status
hue and the label uses #633c3c (6.62:1 / 7.51:1) in both variants.

White on ssa-red (#f85b76) is 3.11:1, under AA for normal text. Added
--color-ssa-red-dark (#c8324c, 5.23:1) — same hue, dark enough for white —
and used it for the dialog confirm, the sticky save bar, the error toast and
the delete link's hover.

The email under the name and the LOG OUT label are both 16px normal text in
#8c8880 (3.42:1), and LOG OUT's new hover put white on #8c8880 (3.53:1).
Both move to #706f6f, the token this PR already adopted for small labels
(4.86:1 idle, 5.01:1 hover).

Left alone, having checked each: the 24px bold name and card headings pass
AA large at 3.02:1 and 3.34:1, and the icon-only cancel button, select
chevron, toggle track and focus rings all clear the 3:1 non-text threshold.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 15, 2026 03:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Moderate accessibility issues remain with insufficient component-boundary contrast.

Review details

Suppressed comments (7)

Previously missed (1) — in code that hasn't changed since the last review.

web/src/app/profile/_components/ProfileClient.tsx:134

  • At 40% opacity this border composites to roughly #d1cbc1 against the cream card, only about 1.5:1, while the white input fill has almost no contrast with the card. The edit controls therefore lack the 3:1 boundary required by the PR's non-text contrast target; use an opaque/darker border or another visible field boundary.

This issue also appears in the following locations of the same file:

  • line 232
  • line 255
  • line 270
  • line 330
  • line 389

web/src/app/profile/_components/ProfileClient.tsx:255

  • The expired dot still uses #f85b76 on #ffccd5, the 2.20:1 pair identified above. Because this dot is the status indicator, it does not meet the claimed 3:1 non-text contrast; use the darker red token for the dot as well (the label can remain #633c3c).
  expired: { pill: 'bg-ssa-pink-light text-ssa-cta-text', dot: 'bg-ssa-red' },

web/src/app/profile/_components/ProfileClient.tsx:330

  • The success toast still renders normal-sized text as white on Tailwind green-600 (about 3.3:1), below the PR's stated 4.5:1 text-contrast target. Use a darker green background (for example green-800) or a sufficiently dark success-text color here as well.
          ? 'bg-green-600 text-ssa-white'

web/src/app/profile/_components/ProfileClient.tsx:389

  • The delete-dialog alert icon is now #f85b76 on #ffccd5, the same 2.20:1 red/pink pair that the status note identifies as below the 3:1 non-text target. Since this icon is exposed as part of the dialog content, use the darker red token here too.
            <FiAlertTriangle className="h-7 w-7 text-ssa-red" />

web/src/app/profile/_components/ProfileClient.tsx:232

  • The off-state switch track is ssa-muted-taupe at 40% opacity over the cream card, which composites to roughly 1.5:1 against the card. As the track/knob is the visual state indicator, this misses the PR's 3:1 non-text target and is difficult to distinguish; use an opaque darker taupe (or another 3:1 token) for the off state.
          value ? 'bg-ssa-red' : 'bg-ssa-muted-taupe/40'

web/src/app/profile/_components/ProfileClient.tsx:270

  • The unknown-state dot is #8c8880 on #ece8df, only about 2.9:1, so this newly added fallback also misses the 3:1 contrast target for the status indicator. Use a darker dot color (for example ssa-grey) while keeping the muted pill background.
  dot: 'bg-ssa-muted-taupe',

web/src/app/profile/_components/ProfileClient.tsx:438

  • The cancel button's 70%-opaque taupe border remains below 3:1 against the dialog background, so it does not provide an accessible component boundary when the button is idle. Use an opaque/darker border to meet the non-text contrast requirement.
            className="w-full rounded-full border border-ssa-muted-taupe/70 py-2.5 font-be-vietnam-pro text-sm font-semibold text-ssa-grey transition-colors hover:border-ssa-grey disabled:opacity-60"
  • Files reviewed: 4/4 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@oorjagandhi
oorjagandhi requested a review from joengy September 16, 2026 21:30
joengy
joengy previously approved these changes Sep 17, 2026

@joengy joengy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code is fine, just need to fix conflict and its ready to merge

Robin-deng617
Robin-deng617 previously approved these changes Sep 17, 2026
Copilot AI lite review requested due to automatic review settings October 11, 2026 05:35
@joengy
joengy dismissed stale reviews from Robin-deng617 and themself via 7a47cac October 11, 2026 05:35
@vercel

vercel Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ssa Ready Ready Preview Oct 11, 2026 5:42am UTC

@joengy
joengy merged commit bd6d7f9 into main Oct 11, 2026
5 of 6 checks passed
@joengy
joengy deleted the feature/user-info-page-151 branch October 11, 2026 05:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The success toast text and returning-member switch still miss their stated contrast targets.

2 open findings

🧠 Review effort: Lite

onClick={() => onChange(!value)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-ssa-red focus:ring-offset-1 ${
value ? 'bg-ssa-red' : 'bg-gray-200'
value ? 'bg-ssa-red' : 'bg-ssa-muted-taupe/40'
isSuccess ? 'bg-green-600 text-white' : 'bg-ssa-red text-white'
className={`fixed top-6 left-1/2 z-50 flex -translate-x-1/2 items-center gap-3 rounded-2xl px-5 py-3 font-be-vietnam-pro shadow-xl transition-all duration-300 ${
isSuccess
? 'bg-green-600 text-ssa-white'

This branch was successfully deployed

1 active deployment
Preview — 7a47cacf Deployed Oct 11, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

User info page

4 participants