TryGhost/Ghost

View on GitHub
apps/comments-ui/src/components/content/Avatar.tsx

Summary

Maintainability
A
3 hrs
Test Coverage
import {ReactComponent as AvatarIcon} from '../../images/icons/avatar.svg';
import {Comment, useAppContext} from '../../AppContext';
import {getInitials} from '../../utils/helpers';

function getDimensionClasses() {
    return 'w-9 h-9 sm:w-[40px] sm:h-[40px]';
}

export const BlankAvatar = () => {
    const dimensionClasses = getDimensionClasses();
    return (
        <figure className={`relative ${dimensionClasses}`}>
            <div className={`flex items-center justify-center rounded-full bg-[rgba(0,0,0,0.085)] dark:bg-[rgba(255,255,255,0.15)] ${dimensionClasses}`}>
                <AvatarIcon className="stroke-white opacity-80" />
            </div>
        </figure>
    );
};

type AvatarProps = {
    comment?: Comment;
};
export const Avatar: React.FC<AvatarProps> = ({comment}) => {
    const {member, avatarSaturation, t} = useAppContext();
    const dimensionClasses = getDimensionClasses();

    const memberName = member?.name ?? comment?.member?.name;

    const getHashOfString = (str: string) => {
        let hash = 0;
        for (let i = 0; i < str.length; i++) {
            hash = str.charCodeAt(i) + ((hash << 5) - hash);
        }
        hash = Math.abs(hash);
        return hash;
    };

    const normalizeHash = (hash: number, min: number, max: number) => {
        return Math.floor((hash % (max - min)) + min);
    };

    const generateHSL = (): [number, number, number] => {
        const commentMember = (comment ? comment.member : member);

        if (!commentMember || !commentMember.name) {
            return [0,0,10];
        }

        const saturation = avatarSaturation === undefined || isNaN(avatarSaturation) ? 50 : avatarSaturation;

        const hRange = [0, 360];
        const lRangeTop = Math.round(saturation / (100 / 30)) + 30;
        const lRangeBottom = lRangeTop - 20;
        const lRange = [lRangeBottom, lRangeTop];

        const hash = getHashOfString(commentMember.name);
        const h = normalizeHash(hash, hRange[0], hRange[1]);
        const l = normalizeHash(hash, lRange[0], lRange[1]);

        return [h, saturation, l];
    };

    const HSLtoString = (hsl: [number, number, number]) => {
        return `hsl(${hsl[0]}, ${hsl[1]}%, ${hsl[2]}%)`;
    };

    const commentGetInitials = () => {
        if (comment && !comment.member) {
            return getInitials(t('Deleted member'));
        }

        const commentMember = (comment ? comment.member : member);

        if (!commentMember || !commentMember.name) {
            return getInitials(t('Anonymous'));
        }
        return getInitials(commentMember.name);
    };

    const commentMember = (comment ? comment.member : member);

    const bgColor = HSLtoString(generateHSL());
    const avatarStyle = {
        background: bgColor
    };

    const avatarEl = (
        <>
            {memberName ?
                (<div className={`flex items-center justify-center rounded-full ${dimensionClasses}`} data-testid="avatar-background" style={avatarStyle}>
                    <p className="font-sans text-lg font-semibold text-white">{ commentGetInitials() }</p>
                </div>) :
                (<div className={`flex items-center justify-center rounded-full bg-neutral-900 dark:bg-[rgba(255,255,255,0.7)] ${dimensionClasses}`} data-testid="avatar-background" >
                    <AvatarIcon className="stroke-white dark:stroke-[rgba(0,0,0,0.6)]" />
                </div>)}
            {commentMember && <img alt="Avatar" className={`absolute left-0 top-0 rounded-full ${dimensionClasses}`} src={commentMember.avatar_image}/>}
        </>
    );

    return (
        <figure className={`relative ${dimensionClasses}`}>
            {avatarEl}
        </figure>
    );
};