From de913a647b234fcebfb11bfd4f10a110ed73a924 Mon Sep 17 00:00:00 2001 From: Santiago Suarez Date: Fri, 12 Jun 2026 13:59:34 -0500 Subject: [PATCH] feat: render initials avatar as default instead of generic icon When the user has no profile photo, render a colored circle with the username's first letter instead of the bundled grey SVG. Initials and background color are derived deterministically from the username (hash into a 10-color WCAG-safe palette), entirely client-side: no API calls, no image generation, no storage. The username was already available one level above each Avatar usage (desktop, mobile, and Studio user menus); it is now threaded down as a new optional prop. Logged-out surfaces keep the generic icon. Part of the default-avatar work discussed in openedx/openedx-platform#38638 (option 1: local fallback per surface). Co-Authored-By: Claude Fable 5 --- src/Avatar.jsx | 30 +++++++++++++++---- src/avatarUtils.js | 31 ++++++++++++++++++++ src/desktop-header/DesktopUserMenuToggle.jsx | 2 +- src/mobile-header/MobileUserMenuToggle.jsx | 5 +++- src/studio-header/UserMenu.tsx | 18 ++++++------ 5 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 src/avatarUtils.js diff --git a/src/Avatar.jsx b/src/Avatar.jsx index 2936ce9117..272a88b889 100644 --- a/src/Avatar.jsx +++ b/src/Avatar.jsx @@ -2,18 +2,36 @@ import React from 'react'; import PropTypes from 'prop-types'; import { AvatarIcon } from './Icons'; +import { getAvatarColor, getInitial } from './avatarUtils'; const Avatar = ({ size, src, alt, className, + username, }) => { - const avatar = src ? ( - {alt} - ) : ( - - ); + let avatar; + if (src) { + avatar = {alt}; + } else if (username) { + avatar = ( + + {getInitial(username)} + + ); + } else { + avatar = ; + } return ( { + let hash = 0; + for (let i = 0; i < username.length; i += 1) { + hash = ((hash << 5) - hash + username.charCodeAt(i)) | 0; // eslint-disable-line no-bitwise + } + return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length]; +}; + +/** + * Returns the uppercased first character of the username. + */ +export const getInitial = (username) => username.charAt(0).toUpperCase(); diff --git a/src/desktop-header/DesktopUserMenuToggle.jsx b/src/desktop-header/DesktopUserMenuToggle.jsx index f9057141c3..99e4003781 100644 --- a/src/desktop-header/DesktopUserMenuToggle.jsx +++ b/src/desktop-header/DesktopUserMenuToggle.jsx @@ -5,7 +5,7 @@ import Avatar from '../Avatar'; const DesktopUserMenuToggle = ({ avatar, label }) => ( <> - + {label} ); diff --git a/src/mobile-header/MobileUserMenuToggle.jsx b/src/mobile-header/MobileUserMenuToggle.jsx index 7f2eeb9114..c65c3b2f5d 100644 --- a/src/mobile-header/MobileUserMenuToggle.jsx +++ b/src/mobile-header/MobileUserMenuToggle.jsx @@ -2,11 +2,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import Avatar from '../Avatar'; -const MobileUserMenuToggle = ({ avatar, username }) => ; +const MobileUserMenuToggle = ({ avatar, username, label }) => ( + +); export const MobileUserMenuTogglePropTypes = { avatar: PropTypes.string, username: PropTypes.string, + label: PropTypes.string, }; MobileUserMenuToggle.propTypes = MobileUserMenuTogglePropTypes; diff --git a/src/studio-header/UserMenu.tsx b/src/studio-header/UserMenu.tsx index 66add10cb4..41877b9bab 100644 --- a/src/studio-header/UserMenu.tsx +++ b/src/studio-header/UserMenu.tsx @@ -1,9 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { - Avatar, -} from '@openedx/paragon'; +import Avatar from '../Avatar'; import NavDropdownMenu from './NavDropdownMenu'; import getUserMenuItems from './utils'; @@ -24,12 +22,14 @@ const UserMenu = ({ data-testid="avatar-image" /> ) : ( - + + + ); const title = isMobile ? avatar : <>{avatar}{username};