Skip to content

feat(profile): About bio, settings UX, and updateUser (1/4) - #434

Merged
motirebuma merged 4 commits into
mainfrom
profile-bio
Jul 24, 2026
Merged

feat(profile): About bio, settings UX, and updateUser (1/4)#434
motirebuma merged 4 commits into
mainfrom
profile-bio

Conversation

@niamao

@niamao niamao commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Stack

Part 1 of 4 — merge this first.

Order Branch PR
1 profile-bio this PR (#434)
2 avatar-consistency #435
3 presence-persist #436
4 api-wiring #437

Related issue: #362

Summary

  • Backend updateUser / updateUserAvatar with bio validation (plain text, max length)
  • Profile About tab + Edit Profile → settings
  • Settings: About field, theme toggle fix, profile background Save dirty state, toast close on the right
  • Login response includes bio (and avatar for later stack parts)

CI follow-ups (on this branch)

  • Lint: set-state-in-effect in theme / profile background hooks
  • Types: unused @ts-expect-error, staged-chat avatar coercion, settings avatar URL parse

Test plan

  • Edit About in settings → save → appears on profile About tab
  • HTML in About is rejected
  • Dark mode toggles both ways and stays until Save
  • Changing only banner color/pattern enables Save Changes
  • Toast close button appears on the right

Add bio validation and updateUser/updateUserAvatar mutations, surface About
on profile/settings, and fix theme toggle, background Save dirty state, and
toast close button placement.

Co-authored-by: Cursor <[email protected]>
@vercel

vercel Bot commented Jul 23, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
quotevote Ready Ready Preview, Comment Jul 24, 2026 8:58am

Use lazy useState init for profile background (matching main) and restore
an eslint disable for intentional themePreference sync on login.

Co-authored-by: Cursor <[email protected]>
Remove unused ts-expect-error after store avatar typing, coerce staged
chat avatar to string, and parse settings avatar objects to URLs.

Co-authored-by: Cursor <[email protected]>

@motirebuma motirebuma 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.

hey @niamao nice work, this is a very well put-together PR! Good breadth of tests, thorough backend validation, a few things to tidy up but nothing major. Let me put together some thoughts below.

What's good

  • Bio validation via normalizeBio() with html rejection, length limiting, and trimming is exactly what we need for user-supplied plaintext. The regex pattern is also simple and straightforward (/<\/?[a-z][\s\S]>/i is a good match for any HTML tag). Symmetric validation on the BE and FE (same 500 character limit, HTML escaping) means the user gets immediate feedback in the UI while the stricter checks (length, HTML) are still enforced on the server. Great work!

  • updateUser resolver has good checks in place: user must sign in, can only update their own account (except for contributorBadge which is admin-only), uniqueness checks for username/email, and uses bcrypt to update the password. Also good that you did $set the fields rather than replacing the entire document, to avoid accidental deletion of fields that the user didn't submit

  • updateUserAvatar resolver looks good - separates avatar update logic from the user update, properly validates input. All good!

  • UX improvements to the settings page: char counter for bio, dirty checking for profile background (pattern/color), using setTheme to apply theme rather than toggleTheme, moving toast close button to the right, etc - all very good details!

  • Unit tests for the bio validation, resolver tests for updateUser (auth, owns account, admin, bad HTML), tests for updateUserAvatar, and settings page e2e tests all look good.

Things to address

  • ReputationDisplay.formatDate has a type mismatch: you've changed the component to accept dateValue: string | Date | null | undefined and in UserReputation.ts it seems like the backend resolver is also handling numbers as dates (e.g. lastCalculated in 'parses numeric timestamps for lastCalculated' test). However, in the component, the formatDate function now expects string | Date input, but the resolver returns empty string for invalid dates. If the resolver returns empty string, that would show up as "Not available" in the reputation display which is correct, but it's possible that the test 'parses numeric timestamps for lastCalculated' is passing because the resolver is getting a string representation of a number (e.g. "1645679200") and then .lean() is turning that into a number. I think it would be good to make sure that the API is returning strings (via .toISOString() or something similar) rather than numbers, since the client code expects string | Date.

  • ThemeContextProvider subscribes to themePreference but doesn't actually use it to set the theme anywhere - I don't think you've used themePreference in the ThemeContextProvider in this PR, because you set const themePreference = useAppStore(...) but the diff doesn't show any use of that variable. The comment says "store will hydrate on mount", but if the user updates their device settings (e.g. enables dark mode), do we want their theme to change? If not, that's fine, but then why subscribe to themePreference at all?

  • The login response now includes avatar and bio - in the addCreatorToUser change, I think it's a good idea to include avatar in the login response, however, the type for the avatar is string | null in the response while the actual field in the database is an object (avataaars qualities). Is the avatar in the login response stringified JSON or is it sent as an object directly (via JSON.stringify() in the JWT)? Either way, make sure that the client code can handle it correctly.

Nitpicks (non-critical)

  • ProfileController now checks !targetUsername || loading before rendering - this is a good change to prevent hydration errors, but targetUsername should be empty only when the route param and the store's username are both empty, i.e. the user is not authenticated and is trying to visit /dashboard/profile directly. In that case, it would be better to redirect them to the dashboard/login rather than show a loading spinner indefinitely.

  • Changed "Return to homepage" link from /search to / - looks good since the app now uses / rather than /search as the homepage.

Thank you @niamao

@flyblackbox @niamao

Serialize reputation dates to ISO strings on the API, clarify theme
preference sync is account-based (not OS), type login avatar as object
or string, and redirect empty /dashboard/profile to login after hydrate.

Co-authored-by: Cursor <[email protected]>
@niamao

niamao commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed review feedback in 657f220:

  1. Reputation dates — GraphQL lastCalculated / created / updated now always serialize via toIsoDateString() (ISO-8601 strings; never raw epoch numbers). Client formatDate expects strings with light defensive parsing for digit-only legacy payloads.
  2. ThemeContext themePreference — it is used: syncs account preference on login / settings Save. Clarified in comments that we intentionally do not follow OS/device color-scheme.
  3. Login avatar typingLoginApiResponse / AuthUser now allow string | Record<string, unknown> | null (JSON body object, not JWT). Matches Mongo avataaars qualities.
  4. Empty /dashboard/profile — after persist hydration, redirects to /auths/login instead of an indefinite loading spinner.

@motirebuma @flyblackbox

@motirebuma motirebuma 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.

hey @niamao, I've reviewed the latest changes and noticed all the previous issues were resolved. Well done with the follow-up commit.

I've double-checked all the items mentioned in my initial review below and didn't find any recurring issues this time.

Previously raised items

  1. ThemeContextProvider themePreference - FIXED. The theme preference from Zustand store is correctly picked up in a useEffect and persisted in localStorage via writeStoredThemeMode(). Cross-device theme consistency should be working now

  2. ProfileController redirect - FIXED. The useSyncExternalStore() correctly picks up the hydration status of Zustand persist and the router is redirected to /auths/login without showing the loading spinner when there's no username available.

  3. Login avatar handling - the addCreatorToUser change correctly passes the avatar object (not stringified) to the client, and the test confirms that both avataaars quality objects and URLs are accepted and handled properly by the frontend.

What's good (not changed from my original review)

Bio validation via normalizeBio() - consistent checks on both frontend and backend, HTML escaping, max length and trim

updateUser resolver - user ownership + auth checks, contributorBadge permission logic, name and username uniqueness validation, password encryption with bcrypt

updateUserAvatar resolver - proper separation of concerns, input validation

Settings UX - bio textarea with character counter, dirty-checking for background color/pattern, theme preference checkbox fixed, toast close button on the right

Tests coverage - all resolvers and settings frontend have necessary tests

Verdict: approved, ready to merge.

Thank you @niamao

@flyblackbox @niamao

@motirebuma
motirebuma merged commit 6102f25 into main Jul 24, 2026
5 checks passed
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.

2 participants