Skip to content

Commit

Permalink
feat(organizations): Add isEmpty to avatar component and use it for m…
Browse files Browse the repository at this point in the history
…embers table TASK-1420 (#5457)

### 📣 Summary
Ghost variant was added to display a "ghost" state in members table when
user is invited but still not active

### 📖 Description
- Added a `isEmpty` prop for our existent `Avatar` component for it to
render the empty-dashed circle.
- At first I intended to add theme and use Mantine's Avatar component,
but since our avatar has so many specific details the effort wouldn't be
worth it, so I just customized the existent component.
- In the future we can migrate the internal structure of our avatar
component to use Mantine's layout components.
- Avatar story was changed to add the isEmpty as a config option

### 👀 Preview steps
One can check the implementation result in Avatar's storybook story
For reference, I'm adding a screenshot of the property in use:

![image](https://github.com/user-attachments/assets/70b8b366-8c26-49ac-aff9-cff27cda8ffa)
  • Loading branch information
pauloamorimbr authored Jan 30, 2025
1 parent fa0edf5 commit f1e67c7
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 31 deletions.
21 changes: 21 additions & 0 deletions jsapp/js/components/common/avatar.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,26 @@ $avatar-size-m: 32px;
.avatar-size-s {@include avatar-size($avatar-size-s);}
.avatar-size-m {@include avatar-size($avatar-size-m);}

.empty {
svg {
width: 100%;
height: 100%;
position: absolute; // Needed to avoid a slight shift when the circle is empty
left: 0;
top: 0;
}

svg circle {
fill: transparent;
stroke: colors.$kobo-gray-400;
stroke-dasharray: 3.14px;
stroke-width: 1px;
}

// Need to make sure that the transparent will be applied when ghost
background-color: transparent !important;
}

.initials {
text-align: center;
text-transform: uppercase;
Expand All @@ -40,6 +60,7 @@ $avatar-size-m: 32px;
color: colors.$kobo-white;
// actual background color is provided by JS, this is just safeguard
background-color: colors.$kobo-storm;
position: relative;
}

.text {
Expand Down
16 changes: 6 additions & 10 deletions jsapp/js/components/common/avatar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,22 @@ export default {
isUsernameVisible: {type: 'boolean'},
hasFullName: {
type: 'boolean',
description: 'Allows testing `fullName` being empty string or not existing',
description:
'Allows testing `fullName` being empty string or not existing',
},
fullName: {type: 'string', if: {arg: 'hasFullName', truthy: true}},
hasEmail: {
type: 'boolean',
description: 'Allows testing `email` being empty string or not existing',
},
email: {type: 'string', if: {arg: 'hasEmail', truthy: true}},
isEmpty: {
type: 'boolean',
},
},
} as ComponentMeta<typeof Avatar>;

const Template: ComponentStory<typeof Avatar> = (args) => (
<Avatar
size={args.size}
username={args.username}
isUsernameVisible={args.isUsernameVisible}
fullName={args.fullName}
email={args.email}
/>
);
const Template: ComponentStory<typeof Avatar> = (args) => <Avatar {...args} />;

export const Simple = Template.bind({});
Simple.args = {
Expand Down
50 changes: 29 additions & 21 deletions jsapp/js/components/common/avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,49 +32,57 @@ interface AvatarProps {
isUsernameVisible?: boolean;
fullName?: string;
email?: string;
isEmpty?: boolean;
}

/**
* Displays an avatar (a letter in a circle) and optionally also username, full
* name and email.
*/
export default function Avatar(props: AvatarProps) {
const isAnyTextBeingDisplayed = (
const isAnyTextBeingDisplayed =
props.isUsernameVisible ||
props.fullName !== undefined ||
props.email !== undefined
);
props.email !== undefined;

return (
<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>
{props.isEmpty ? (
<div className={cx(styles.initials, styles.empty)}>
&nbsp; {/* Empty space to keep the div from shifting sizes for being empty */}
<svg viewBox='0 0 24 24'>
<circle cx='12' cy='12' r='11' />
</svg>
</div>
) : (
<div
className={styles.initials}
style={{backgroundColor: `${stringToHSL(props.username, 80, 40)}`}}
>
{props.username.charAt(0)}
</div>
)}

{isAnyTextBeingDisplayed &&
{isAnyTextBeingDisplayed && (
<div
className={cx(
styles.text,
{[styles.hasFullName]: props.fullName !== undefined}
)}
className={cx(styles.text, {
[styles.hasFullName]: props.fullName !== undefined,
})}
>
{props.fullName !== undefined &&
{props.fullName !== undefined && (
<span className={styles.fullName}>{props.fullName}</span>
}
)}

{/* Sometimes will be prefixed with "@" symbol */}
{props.isUsernameVisible &&
{props.isUsernameVisible && (
<span className={styles.username}>{props.username}</span>
}
)}

{props.email !== undefined &&
{props.email !== undefined && (
<div className={styles.email}>{props.email}</div>
}
)}
</div>
}
)}
</div>
);
}

0 comments on commit f1e67c7

Please sign in to comment.