Skip to content

Commit

Permalink
chore: add name initials to gradient circle
Browse files Browse the repository at this point in the history
  • Loading branch information
DenisVorop committed Aug 16, 2023
1 parent c220573 commit cbcc0dd
Show file tree
Hide file tree
Showing 6 changed files with 55 additions and 45 deletions.
61 changes: 31 additions & 30 deletions src/components/Gravatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,59 +3,67 @@ import md5Hash from 'md5';
import styled from 'styled-components';

import { isRetina } from '../utils/isRetina';
import { stringToColor } from '../utils/stringToColor';
import { isColorDark, stringToColor } from '../utils/stringToColor';

interface GravatarProps extends React.HTMLAttributes<HTMLImageElement> {
email: string;
email?: string | null;
md5?: string;
size: number;
rating?: string;
name?: string | null;
def?: string;
className?: string;
domain?: string;

onClick?: () => void;
}

const StyledImage = styled.img<{ visible: boolean }>`
const StyledImage = styled.img`
border: 0;
border-radius: 100%;
opacity: 1;
transition: opacity 50ms ease-in;
${({ visible }) =>
!visible &&
`
opacity: 0;
`}
`;

const Circle = styled.div<{ size: number; str: string }>`
border-radius: 100%;
${({ size, str }) => {
const mainColor = stringToColor(str);
return {
width: `${size}px`,
height: `${size}px`,
background: `linear-gradient(90deg,${stringToColor(str.toLowerCase())},${stringToColor(
str.toUpperCase(),
)})`,
background: `linear-gradient(90deg,${mainColor},${stringToColor(str.toUpperCase())})`,
color: `${isColorDark(mainColor) ? 'white' : 'black'}`,
fontSize: `calc(${size / 2}px - 2px)`,
};
}};
display: flex;
align-items: center;
justify-content: center;
letter-spacing: -1.5px;
font-weight: 600;
user-select: none;
`;

const getInitials = (fullName?: string | null) => {
if (!fullName) return '';
const arr = fullName.split(' ');
return `${arr[0]?.[0] || ''} ${arr[1]?.[0] || ''}`;
};

export const Gravatar: FC<GravatarProps> = ({
size = 50,
rating = 'g',
def = 'retro',
domain = process.env.NEXT_PUBLIC_GRAVATAR_HOST || 'www.gravatar.com',
email,
md5,
name,
...props
}) => {
const [modernBrowser, setModernBrowser] = useState(true);
const [mounted, setMounted] = useState(false);
const [visible, setVisible] = useState(false);
const [isError, setIsError] = useState(false);
const imgRef = useRef<HTMLImageElement>(null);

Expand All @@ -78,11 +86,10 @@ export const Gravatar: FC<GravatarProps> = ({
d: def,
});

const formattedEmail = email.trim().toLowerCase();
const formattedEmail = email?.trim().toLowerCase() || '';

const onLoadError: React.ReactEventHandler<HTMLImageElement> = useCallback(({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = '/anonymous.png';
setIsError(true);
}, []);

Expand All @@ -103,25 +110,19 @@ export const Gravatar: FC<GravatarProps> = ({
useEffect(() => {
if (imgRef.current && mounted) {
imgRef.current.src = modernBrowser && isRetina() ? retinaSrc : src;
setVisible(true);
}
}, [imgRef, mounted, modernBrowser, src, retinaSrc]);

const initials = getInitials(name);

return (
<>
{!isError ? (
<StyledImage
visible={visible}
ref={imgRef}
alt={`Gravatar for ${formattedEmail}`}
src="/anonymous.png"
height={size}
width={size}
onError={onLoadError}
{...props}
/>
<StyledImage ref={imgRef} height={size} width={size} onError={onLoadError} {...props} />
) : (
<Circle size={size} str={email} />
<Circle size={size} str={formattedEmail} {...props}>
{initials}
</Circle>
)}
</>
);
Expand Down
6 changes: 1 addition & 5 deletions src/components/UserGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,6 @@ const UserContainer = styled.div`
}
`;

const StyledUserPic = styled(UserPic)`
display: block;
`;

const StyledCounter = styled.div`
min-width: 0.75rem;
padding: 0px 5px;
Expand Down Expand Up @@ -89,7 +85,7 @@ export const UserGroup: FC<UserGroupProps> = ({ users, size = 24, limit = 3, ...
key={i}
target={
<UserContainer>
<StyledUserPic src={user.image} email={user.email} size={size} />
<UserPic name={user.name} src={user.image} email={user.email} size={size} />
</UserContainer>
}
tooltip
Expand Down
5 changes: 3 additions & 2 deletions src/components/UserMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ interface UserMenuProps extends React.HTMLAttributes<HTMLDivElement> {
notifications?: boolean;
avatar?: string | null;
email?: string | null;
name?: string | null;
onClick?: () => void;
}

Expand Down Expand Up @@ -38,14 +39,14 @@ const StyledNotifier = styled.div`
}
`;

export const UserMenu: React.FC<UserMenuProps> = ({ notifications, avatar, email, onClick, ...attrs }) => {
export const UserMenu: React.FC<UserMenuProps> = ({ name, notifications, avatar, email, onClick, ...attrs }) => {
return (
<StyledMenuWrapper {...attrs}>
{nullable(notifications, () => (
<StyledNotifier />
))}

<UserPic src={avatar} email={email} onClick={onClick} size={32} />
<UserPic name={name} src={avatar} email={email} onClick={onClick} size={32} />
</StyledMenuWrapper>
);
};
2 changes: 1 addition & 1 deletion src/components/UserMenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const StyledUserPic = styled(UserPic)`

export const UserMenuItem: React.FC<UserMenuItemProps> = ({ name, email, image, ...props }) => (
<StyledUserCard {...props}>
<StyledUserPic src={image} email={email} size={24} />
<StyledUserPic src={image} email={email} size={24} name={name} />

<StyledUserInfo>
<StyledUserName>{name}</StyledUserName>
Expand Down
9 changes: 2 additions & 7 deletions src/components/UserPic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface UserPicProps extends React.HTMLAttributes<HTMLImageElement> {
src?: string | null;
size?: number;
email?: string | null;
name?: string | null;
className?: string;

onClick?: () => void;
Expand All @@ -22,17 +23,11 @@ export const UserPic: React.FC<UserPicProps> = ({ src, email, size = 32, ...prop

const onLoadError: React.ReactEventHandler<HTMLImageElement> = useCallback(({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = '/anonymous.png';
}, []);

if (src) {
return <StyledImage src={src} height={sizePx} width={sizePx} onError={onLoadError} {...props} />;
}

if (email) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return <Gravatar email={email} size={Number(sizePx.split('px')[0])} {...props} />;
}

return <StyledImage src="/anonymous.png" height={sizePx} width={sizePx} {...props} />;
return <Gravatar email={email} size={size} {...props} />;
};
17 changes: 17 additions & 0 deletions src/utils/stringToColor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,20 @@ export const stringToColor = (str: string) => {
}
return color;
};

const hexToRgb = (hex: string) => {
hex = hex.replace(/^#/, '');

const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);

return `rgb(${r}, ${g}, ${b})`;
};

export const isColorDark = (color: string) => {
const rgb = (hexToRgb(color).match(/\d+/g) || []).map(Number);
const brightness = (0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]) / 255;

return brightness < 0.5;
};

0 comments on commit cbcc0dd

Please sign in to comment.