Skip to content

Commit

Permalink
Merge pull request #5168 from kobotoolbox/task-1154-avatar-component
Browse files Browse the repository at this point in the history
[TASK-1154] Update Avatar component
  • Loading branch information
pauloamorimbr authored Oct 15, 2024
2 parents b646a1d + c1e5d37 commit 7356674
Show file tree
Hide file tree
Showing 16 changed files with 158 additions and 139 deletions.
12 changes: 0 additions & 12 deletions jsapp/js/account/accountSettings.scss
Original file line number Diff line number Diff line change
Expand Up @@ -39,18 +39,6 @@
.form-modal__item--username {
min-height: 32px;
padding-left: sizes.$x50;

.account-box__initials {
display: inline-block;
vertical-align: middle;
margin-right: 15px;
}

h4 {
display: inline-block;
vertical-align: middle;
font-size: 16px;
}
}
}

Expand Down
13 changes: 3 additions & 10 deletions jsapp/js/account/accountSettingsRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import {unstable_usePrompt as usePrompt} from 'react-router-dom';
import bem, {makeBem} from 'js/bem';
import sessionStore from 'js/stores/session';
import './accountSettings.scss';
import {notify, stringToColor} from 'js/utils';
import {notify} from 'js/utils';
import {dataInterface} from '../dataInterface';
import AccountFieldsEditor from './accountFieldsEditor.component';
import Icon from 'js/components/common/icon';
import Avatar from 'js/components/common/avatar';
import envStore from 'js/envStore';
import {
getInitialAccountFieldsValues,
Expand Down Expand Up @@ -134,9 +134,6 @@ const AccountSettings = observer(() => {
};

const accountName = sessionStore.currentAccount.username;
const initialsStyle = {
background: `#${stringToColor(accountName)}`,
};

return (
<bem.AccountSettings onSubmit={updateProfile}>
Expand All @@ -152,11 +149,7 @@ const AccountSettings = observer(() => {

<bem.AccountSettings__item m={'column'}>
<bem.AccountSettings__item m='username'>
<bem.AccountBox__initials style={initialsStyle}>
{accountName.charAt(0)}
</bem.AccountBox__initials>

<h4>{accountName}</h4>
<Avatar size='m' username={accountName} isUsernameVisible/>
</bem.AccountSettings__item>

{sessionStore.isInitialLoadComplete && form.isUserDataLoaded && (
Expand Down
8 changes: 2 additions & 6 deletions jsapp/js/account/changePasswordRoute.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import DocumentTitle from 'react-document-title';
import {observer} from 'mobx-react';
import sessionStore from 'js/stores/session';
import bem, {makeBem} from 'js/bem';
import {stringToColor} from 'js/utils';
import {withRouter} from 'js/router/legacy';
import type {WithRouterProps} from 'jsapp/js/router/legacy';
import './accountSettings.scss';
import styles from './changePasswordRoute.module.scss';
import UpdatePasswordForm from './security/password/updatePasswordForm.component';
import Button from 'js/components/common/button';
import Avatar from 'js/components/common/avatar';

bem.AccountSettings = makeBem(null, 'account-settings');
bem.AccountSettings__left = makeBem(bem.AccountSettings, 'left');
Expand All @@ -28,7 +28,6 @@ const ChangePasswordRoute = class ChangePassword extends React.Component<WithRou
}

const accountName = sessionStore.currentAccount.username;
const initialsStyle = {background: `#${stringToColor(accountName)}`};

return (
<DocumentTitle title={`${accountName} | KoboToolbox`}>
Expand All @@ -44,10 +43,7 @@ const ChangePasswordRoute = class ChangePassword extends React.Component<WithRou

<bem.AccountSettings__item m='column'>
<bem.AccountSettings__item m='username'>
<bem.AccountBox__initials style={initialsStyle}>
{accountName.charAt(0)}
</bem.AccountBox__initials>
<h4>{accountName}</h4>
<Avatar size='m' username={accountName} isUsernameVisible />
</bem.AccountSettings__item>

<div className={styles.fieldsWrapper}>
Expand Down
1 change: 0 additions & 1 deletion jsapp/js/bemComponents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ bem.LoginBox = makeBem(null, 'login-box');

bem.AccountBox = makeBem(null, 'account-box');
bem.AccountBox__name = makeBem(bem.AccountBox, 'name', 'div');
bem.AccountBox__initials = makeBem(bem.AccountBox, 'initials', 'span');
bem.AccountBox__menu = makeBem(bem.AccountBox, 'menu', 'ul');
bem.AccountBox__menuLI = makeBem(bem.AccountBox, 'menu-li', 'li');
bem.AccountBox__menuItem = makeBem(bem.AccountBox, 'menu-item', 'div');
Expand Down
40 changes: 30 additions & 10 deletions jsapp/js/components/common/avatar.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,45 @@
@use 'scss/colors';
@use 'scss/mixins';

.root {
$avatar-size-s: 24px;
$avatar-size-m: 32px;
$avatar-size-l: 48px;

@mixin avatar-size($size) {
// This is the gap between the initials and the (optional) username
gap: $size * 0.25;

.initials {
width: $size;
height: $size;
border-radius: $size;
line-height: $size;
font-size: $size * 0.6;
}
}

.avatar {
@include mixins.centerRowFlex;
}

.avatar-size-s {
@include avatar-size($avatar-size-s);
}

.avatar-size-m {
@include avatar-size($avatar-size-m);
}

.avatar-size-l {
@include avatar-size($avatar-size-l);
}

.initials {
width: sizes.$x20;
text-align: center;
text-transform: uppercase;
padding: 0 sizes.$x6;
border-radius: sizes.$x20;
display: inline-block;
vertical-align: middle;
line-height: sizes.$x20;
font-size: sizes.$x12;
color: colors.$kobo-white;
// actual background color is provided by JS, this is just safeguard
background-color: colors.$kobo-storm;

&:not(:last-child) {
margin-right: sizes.$x5;
}
}
60 changes: 60 additions & 0 deletions jsapp/js/components/common/avatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React from 'react';
import type {ComponentStory, ComponentMeta} from '@storybook/react';

import Avatar from './avatar';
import type {AvatarSize} from './avatar';

const avatarSizes: AvatarSize[] = ['s', 'm', 'l'];

export default {
title: 'common/Avatar',
component: Avatar,
argTypes: {
username: {type: 'string'},
size: {
options: avatarSizes,
control: {type: 'select'},
},
isUsernameVisible: {type: 'boolean'},
},
} as ComponentMeta<typeof Avatar>;

const Template: ComponentStory<typeof Avatar> = (args) => <Avatar {...args} />;

export const Primary = Template.bind({});
Primary.args = {
username: 'Leszek',
size: avatarSizes[0],
isUsernameVisible: true,
};

// We want to test how the avatar colors look like with some ~random usernames.
const bulkUsernames = [
// NATO phonetic alphabet (https://en.wikipedia.org/wiki/NATO_phonetic_alphabet)
'Alfa', 'Bravo', 'Charlie', 'Delta', 'Echo', 'Foxtrot', 'Golf', 'Hotel',
'India', 'Juliett', 'Kilo', 'Lima', 'Mike', 'November', 'Oscar', 'Papa',
'Quebec', 'Romeo', 'Sierra', 'Tango', 'Uniform', 'Victor', 'Whiskey', 'Xray',
'Yankee', 'Zulu',
// Top 100 most popular names in the world (https://forebears.io/earth/forenames)
'Maria', 'Nushi', 'Mohammed', 'Jose', 'Muhammad', 'Mohamed', 'Wei', 'Mohammad',
'Ahmed', 'Yan', 'Ali', 'John', 'David', 'Li', 'Abdul', 'Ana', 'Ying', 'Michael',
'Juan', 'Anna', 'Mary', 'Jean', 'Robert', 'Daniel', 'Luis', 'Carlos', 'James',
'Antonio', 'Joseph', 'Hui', 'Elena', 'Francisco', 'Hong', 'Marie', 'Min', 'Lei',
'Yu', 'Ibrahim', 'Peter', 'Fatima', 'Aleksandr', 'Richard', 'Xin', 'Bin',
'Paul', 'Ping', 'Lin', 'Olga', 'Sri', 'Pedro', 'William', 'Rosa', 'Thomas',
'Jorge', 'Yong', 'Elizabeth', 'Sergey', 'Ram', 'Patricia', 'Hassan', 'Anita',
'Manuel', 'Victor', 'Sandra', 'Ming', 'Siti', 'Miguel', 'Emmanuel', 'Samuel',
'Ling', 'Charles', 'Sarah', 'Mario', 'Joao', 'Tatyana', 'Mark', 'Rita',
'Martin', 'Svetlana', 'Patrick', 'Natalya', 'Qing', 'Ahmad', 'Martha', 'Andrey',
'Sunita', 'Andrea', 'Christine', 'Irina', 'Laura', 'Linda', 'Marina', 'Carmen',
'Ghulam', 'Vladimir', 'Barbara', 'Angela', 'George', 'Roberto', 'Peng',
];
export const BulkTest = () => (
<div style={{display: 'flex', flexWrap: 'wrap', gap: '10px'}}>
{bulkUsernames.map((username) => (
<div key={username}>
<Avatar size='m' username={username} isUsernameVisible/>
</div>
))}
</div>
);
37 changes: 33 additions & 4 deletions jsapp/js/components/common/avatar.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,48 @@
import {stringToColor} from 'jsapp/js/utils';
import React from 'react';
import cx from 'classnames';
import styles from './avatar.module.scss';

export type AvatarSize = 'l' | 'm' | 's';

/**
* A simple function that generates hsl color from given string. Saturation and
* lightness is not random, just the hue.
*/
function stringToHSL(string: string, saturation: number, lightness: number) {
let hash = 0;
for (let i = 0; i < string.length; i++) {
hash = string.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash;
}
return `hsl(${(hash % 360)}, ${saturation}%, ${lightness}%)`;
}

interface AvatarProps {
/**
* First letter of the username would be used as avatar. Whole username would
* be used to generate the color of the avatar.
*/
username: string;
/**
* Username is not being displayed by default.
*/
isUsernameVisible?: boolean;
size: AvatarSize;
}

export default function Avatar(props: AvatarProps) {
return (
<div className={styles.root}>
<div className={styles.initials} style={{background: `#${stringToColor(props.username)}`}}>
<div className={cx(styles.avatar, styles[`avatar-size-${props.size}`])}>
<div
className={styles.initials}
style={{backgroundColor: `${stringToHSL(props.username, 80, 40)}`}}
>
{props.username.charAt(0)}
</div>

<label>{props.username}</label>
{props.isUsernameVisible &&
<label>{props.username}</label>
}
</div>
);
}
17 changes: 7 additions & 10 deletions jsapp/js/components/formSummary.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,8 @@ import mixins from 'js/mixins';
import bem from 'js/bem';
import DocumentTitle from 'react-document-title';
import Icon from 'js/components/common/icon';
import Avatar from 'js/components/common/avatar';
import {getFormDataTabs} from './formViewSideTabs';
import {
stringToColor,
} from 'utils';
import {getUsernameFromUrl, ANON_USERNAME} from 'js/users/utils';
import {MODAL_TYPES} from 'js/constants';
import './formSummary.scss';
Expand Down Expand Up @@ -154,13 +152,12 @@ class FormSummary extends React.Component {
}
<bem.FormView__cell m={['box', 'padding']}>
{ team.map((username, ind) =>
<bem.UserRow key={ind}>
<bem.UserRow__avatar data-tip={username}>
<bem.AccountBox__initials style={{background: `#${stringToColor(username)}`}}>
{username.charAt(0)}
</bem.AccountBox__initials>
</bem.UserRow__avatar>
</bem.UserRow>
<Avatar
key={ind}
username={username}
size='s'
isUsernameVisible
/>
)}
</bem.FormView__cell>
</bem.FormView__row>
Expand Down
1 change: 1 addition & 0 deletions jsapp/js/components/formSummary.scss
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
gap: 10px;
}

.user-row {
Expand Down
6 changes: 5 additions & 1 deletion jsapp/js/components/formSummaryProjectInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ export default function FormSummaryProjectInfo(
<bem.FormView__label>{t('Owner')}</bem.FormView__label>
{isSelfOwned(props.asset) && t('me')}
{!isSelfOwned(props.asset) && (
<Avatar username={props.asset.owner__username} />
<Avatar
username={props.asset.owner__username}
size='s'
isUsernameVisible
/>
)}
</bem.FormView__cell>
</bem.FormView__group>
Expand Down
17 changes: 7 additions & 10 deletions jsapp/js/components/header/accountMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import {useNavigate} from 'react-router-dom';
import PopoverMenu from 'js/popoverMenu';
import sessionStore from 'js/stores/session';
import bem from 'js/bem';
import {currentLang, stringToColor} from 'js/utils';
import {currentLang} from 'js/utils';
import envStore from 'js/envStore';
import type {LabelValuePair} from 'js/dataInterface';
import {dataInterface} from 'js/dataInterface';
import {actions} from 'js/actions';
import {ACCOUNT_ROUTES} from 'js/account/routes.constants';
import {isAnyRouteBlockerActive} from 'js/router/routerUtils';
import Button from 'js/components/common/button';
import Avatar from 'js/components/common/avatar';

/**
* UI element that display things only for logged-in user. An avatar that gives
Expand Down Expand Up @@ -78,20 +79,16 @@ export default function AccountMenu() {
? sessionStore.currentAccount.email
: '';

const initialsStyle = {background: `#${stringToColor(accountName)}`};
const accountMenuLabel = (
<bem.AccountBox__initials style={initialsStyle}>
{accountName.charAt(0)}
</bem.AccountBox__initials>
);

return (
<bem.AccountBox>
<PopoverMenu type='account-menu' triggerLabel={accountMenuLabel}>
<PopoverMenu
type='account-menu'
triggerLabel={<Avatar size='m' username={accountName} />}
>
<bem.AccountBox__menu>
<bem.AccountBox__menuLI key='1'>
<bem.AccountBox__menuItem m={'avatar'}>
{accountMenuLabel}
<Avatar size='l' username={accountName} />
</bem.AccountBox__menuItem>

<bem.AccountBox__menuItem m={'mini-profile'}>
Expand Down
4 changes: 0 additions & 4 deletions jsapp/js/components/permissions/sharingForm.scss
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,7 @@ $s-gray-row-spacing: 10px;

.user-row__avatar {
margin-right: 12px;
}

.user-row__name {
flex: 2;
line-height: 2.2em;
}

.user-row__perms {
Expand Down
Loading

0 comments on commit 7356674

Please sign in to comment.