Repository navigation
feat: restyle user info page to match Figma (#151) - #155
Conversation
There was a problem hiding this comment.
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
/profilepage container sizing/spacing tomax-w-3xlwith updated responsive padding. - Reworked
ProfileClientpresentation: new field label/value styling, updated header controls, and redesigned membershipStatusBadge(including an empty-string fallback fix). - Added a new
ProfileCardcomponent 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
EditTextrenders a visual label but the<input>has no programmatic accessible name (the label is a<span>not associated with the control). Add anaria-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
EditSelectrenders a visual label but the<select>has no programmatic accessible name. Add anaria-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
EditTogglehasrole="switch"but no accessible name, so screen readers will announce an unlabeled switch. Addaria-label={label}(oraria-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.
There was a problem hiding this comment.
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
EditTextrenders a visible label but the<input>has no programmatic label (no<label htmlFor>,aria-label, oraria-labelledby). This makes the control harder to use with screen readers. Addingaria-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
EditToggleusesrole="switch"but doesn't provide an accessible name (noaria-label/aria-labelledby). Screen readers will announce it as an unnamed switch. Addingaria-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
EditSelectrenders a visible label but the<select>has no programmatic label (no<label htmlFor>,aria-label, oraria-labelledby). Addingaria-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-codesmax-w-[756px](and the inline comment also describes 756px). Please align either the code to the describedmax-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">
…ut and consistency
joengy
left a comment
There was a problem hiding this comment.
The user profile page looks great, thank you for implementing it so fast.
Robin-deng617
left a comment
There was a problem hiding this comment.
Looks 1 on 1 with the figma. never dobuted you richman. just had 1 question though.
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>
There was a problem hiding this comment.
🟡 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
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>
There was a problem hiding this comment.
🔵 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
#ffccd5and#ffe3ac;#f85b76and#9f7d32yield 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#fffbf4page 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
headerControlIdlerenders it as#8c8880on the light page background (about 3.4:1) and white on#8c8880on 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-600tobg-ssa-redwhile keeping a 14px white label. White on#f85b76is 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>
There was a problem hiding this comment.
🔵 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
#d1cbc1against 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
#f85b76on#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 examplegreen-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
#f85b76on#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-taupeat 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
#8c8880on#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 examplessa-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
joengy
left a comment
There was a problem hiding this comment.
Code is fine, just need to fix conflict and its ready to merge
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| 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' |

What does this PR do?
Restyles
/profileto match the user page design in Figma (node5457-12171). Layout, data and behaviour are unchanged — this is a visual pass over the existing page.ssa-redbold with the email beneath, and the two controls from the mock on the right: an outlined pencil circle and aLOG OUTpill.ProfileCard— cream fill (ssa-card), muted-taupe heading, no rule, and a 2px shadow that barely lifts it off the background. Deliberately not the sharedCardSection: that one is thessa-yellow/ Averia / red-rule treatment and has five other callers (signup steps, sign-in,PaymentStep), so changing it would have redesigned those pages too.ssa-greyvalues, reusing the caption idiom already on the events cards. Two per row, collapsing to one belowsm.max-w-[756px], withgap-8(32px) between the header row and the card stack andgap-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 onmax-w-3xl/ 768px;7441090replaced 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:
#8c8880for 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.LOG OUT, butDELETE /api/auth/meis 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]) ?? fallbackreturns''whenmembershipStatusis an empty string, because??only catchesnull/undefined— so the badge rendered blank instead of "Unknown".Review follow-ups
266e0cdViewFieldchose its placeholder with??, which only catches null/undefined — butuserToFormnormalises 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||.266e0cdFieldLabelnow renders a real<label>when givenhtmlFor; the switch is referenced witharia-labelledby.dad2668ssa-redpair failed in three more places. Status pill labels now use#633c3cwith 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 andLOG OUTlabel move to#706f6f. Every text pair on the page now clears 4.5:1 and every non-text pair 3:1 — full table in1801d98. This adds one palette token and moves the pills off the mock's exact colours, so it wants a design sign-off.1801d98Type of change
Checklist
feature/,fix/,chore/,hotfix/)Verification
Ran the page against a locally minted session (
tsc --noEmit,eslint,next buildall 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 atsmand 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