diff --git a/frontend/src/components/user/avatar.js b/frontend/src/components/user/avatar.js index 715bdf185b..45348c9a02 100644 --- a/frontend/src/components/user/avatar.js +++ b/frontend/src/components/user/avatar.js @@ -4,6 +4,8 @@ import { useSelector } from 'react-redux'; import { ProfilePictureIcon, CloseIcon } from '../svgIcons'; import { getRandomArrayItem } from '../../utils/random'; +import { useAvatarStyle } from '../../hooks/UseAvatarStyle'; +import { useAvatarText } from '../../hooks/UseAvatarText'; export const CurrentUserAvatar = (props) => { const userPicture = useSelector((state) => state.auth.getIn(['userDetails', 'pictureUrl'])); @@ -30,44 +32,48 @@ export const UserAvatar = ({ editMode, disableLink = false, }: Object) => { - let sizeClasses = 'h2 w2 f5'; - let textPadding = editMode ? { top: '-0.75rem' } : { paddingTop: '0.375rem' }; - let sizeStyles = {}; - let closeIconStyle = { left: '0.4rem' }; + const avatarText = useAvatarText(name, username, number); - if (size === 'large') { - closeIconStyle = { marginLeft: '3rem' }; - sizeClasses = 'h3 w3 f2'; - textPadding = editMode ? { top: '-0.5rem' } : { paddingTop: '0.625rem' }; - } - - if (size === 'small') { - closeIconStyle = { marginLeft: '0' }; - sizeClasses = 'f6'; - sizeStyles = { height: '1.5rem', width: '1.5rem' }; - textPadding = editMode ? { top: '-0.5rem' } : { paddingTop: '0.225rem' }; - } - - let letters; - if (name) { - letters = name - .split(' ') - .map((word) => word[0]) - .join(''); - } else if (number) { - letters = number; + if ((removeFn && editMode) || disableLink) { + return ( + + ); } else { - letters = username - .split(' ') - .map((word) => word[0]) - .join(''); + return ( + + + + ); } - if (picture) sizeStyles.backgroundImage = `url(${picture})`; +}; + +const Avatar = ({ username, size, colorClasses, removeFn, picture, text, editMode }) => { + const { sizeClasses, textPadding, closeIconStyle, sizeStyle } = useAvatarStyle( + size, + editMode, + picture, + ); - const avatar = ( + return (
{removeFn && editMode && ( @@ -81,17 +87,11 @@ export const UserAvatar = ({ )} {!picture && ( - {letters.substr(0, 3)} + {text} )}
); - - if ((removeFn && editMode) || disableLink) { - return avatar; - } else { - return {avatar}; - } }; export const UserAvatarList = ({ diff --git a/frontend/src/components/user/tests/userAvatar.test.js b/frontend/src/components/user/tests/userAvatar.test.js index 1ca7fd262c..04278b962a 100644 --- a/frontend/src/components/user/tests/userAvatar.test.js +++ b/frontend/src/components/user/tests/userAvatar.test.js @@ -1,18 +1,21 @@ import React from 'react'; -import TestRenderer from 'react-test-renderer'; +import TestRenderer, { act } from 'react-test-renderer'; import { UserAvatar, UserAvatarList } from '../avatar'; import { CloseIcon } from '../../svgIcons'; describe('UserAvatar', () => { it('with picture url and default size', () => { - const element = TestRenderer.create( - , - ); + let element; + act(() => { + element = TestRenderer.create( + , + ); + }); const elementInstance = element.root; expect(elementInstance.findByProps({ title: 'Mary' }).type).toBe('div'); expect(elementInstance.findByProps({ title: 'Mary' }).props.style.backgroundImage).toBe( - 'url(http://image.xyz/photo.jpg)', + 'url("http://image.xyz/photo.jpg")', ); expect(elementInstance.findByProps({ title: 'Mary' }).props.className).toBe( 'dib mh1 br-100 tc v-mid cover red h2 w2 f5', @@ -20,17 +23,20 @@ describe('UserAvatar', () => { }); it('with picture url and large size', () => { - const element = TestRenderer.create( - , - ); + let element; + act(() => { + element = TestRenderer.create( + , + ); + }); const elementInstance = element.root; expect(elementInstance.findByProps({ title: 'Mary' }).props.style.backgroundImage).toBe( - 'url(http://image.xyz/photo2.jpg)', + 'url("http://image.xyz/photo2.jpg")', ); expect(elementInstance.findByProps({ title: 'Mary' }).props.className).toBe( 'dib mh1 br-100 tc v-mid cover orange h3 w3 f2', @@ -38,9 +44,12 @@ describe('UserAvatar', () => { }); it('without picture url and with large size', () => { - const element = TestRenderer.create( - , - ); + let element; + act(() => { + element = TestRenderer.create( + , + ); + }); const elementInstance = element.root; expect(elementInstance.findByType('div').props.className).toBe( 'dib mh1 br-100 tc v-mid cover white bg-red h3 w3 f2', @@ -52,9 +61,12 @@ describe('UserAvatar', () => { }); it('with name with default size', () => { - const element = TestRenderer.create( - , - ); + let element; + act(() => { + element = TestRenderer.create( + , + ); + }); const elementInstance = element.root; expect(elementInstance.findByType('div').props.className).toBe( 'dib mh1 br-100 tc v-mid cover white bg-red h2 w2 f5', @@ -66,17 +78,27 @@ describe('UserAvatar', () => { }); it('with more than 3 words name', () => { - const element = TestRenderer.create( - , - ); + let element; + act(() => { + element = TestRenderer.create( + , + ); + }); const elementInstance = element.root; expect(elementInstance.findByType('span').props.children).toContain('MPL'); }); it('with username containing space', () => { - const element = TestRenderer.create( - , - ); + let element; + act(() => { + element = TestRenderer.create( + , + ); + }); const elementInstance = element.root; expect(elementInstance.findByType('span').props.children).toContain('MPL'); expect(() => elementInstance.findByType(CloseIcon)).toThrow( @@ -85,13 +107,16 @@ describe('UserAvatar', () => { }); it('with editMode TRUE but without removeFn has NOT a CloseIcon', () => { - const element = TestRenderer.create( - , - ); + let element; + act(() => { + element = TestRenderer.create( + , + ); + }); const elementInstance = element.root; expect(elementInstance.findByType('span').props.children).toContain('MPL'); expect(() => elementInstance.findByType(CloseIcon)).toThrow( @@ -100,13 +125,16 @@ describe('UserAvatar', () => { }); it('with removeFn, but with editMode FALSE has NOT a CloseIcon', () => { - const element = TestRenderer.create( - console.log('no')} - />, - ); + let element; + act(() => { + element = TestRenderer.create( + console.log('no')} + />, + ); + }); const elementInstance = element.root; expect(elementInstance.findByType('span').props.children).toContain('MPL'); expect(() => elementInstance.findByType(CloseIcon)).toThrow( @@ -188,11 +216,13 @@ describe('UserAvatarList', () => { { username: 'osmuser' }, ]; it('large size, with a defined bgColor and without maxLength', () => { - const element = TestRenderer.create( - , - ); + let element; + act(() => { + element = TestRenderer.create(); + }); const elementInstance = element.root; expect(elementInstance.findAllByType(UserAvatar).length).toBe(users.length); + expect(elementInstance.findAllByType(UserAvatar)[0].props.colorClasses).toBe('white bg-red'); expect(elementInstance.findAllByProps({ className: 'dib' }).length).toBe(users.length); expect(elementInstance.findAllByProps({ className: 'dib' })[0].props.style).toStrictEqual({ marginLeft: '', @@ -201,23 +231,21 @@ describe('UserAvatarList', () => { marginLeft: '-1.5rem', }); expect(elementInstance.findAllByType(UserAvatar)[0].props.size).toBe('large'); - expect(elementInstance.findAllByProps({ colorClasses: 'white bg-red' }).length).toBe( - users.length, - ); }); it('small size, with a defined bgColor and textColor', () => { - const element = TestRenderer.create( - , - ); + let element; + act(() => { + element = TestRenderer.create( + , + ); + }); const elementInstance = element.root; expect(elementInstance.findAllByType(UserAvatar).length).toBe(users.length); expect(elementInstance.findAllByType(UserAvatar)[0].props.size).toBe('small'); + expect(elementInstance.findAllByType(UserAvatar)[0].props.colorClasses).toBe('black bg-white'); expect(elementInstance.findAllByProps({ className: 'dib' })[1].props.style).toStrictEqual({ marginLeft: '-0.875rem', }); - expect(elementInstance.findAllByProps({ colorClasses: 'black bg-white' }).length).toBe( - users.length, - ); }); it('default size, without bgColor and with maxLength = 5', () => { const element = TestRenderer.create(); diff --git a/frontend/src/hooks/UseAvatarStyle.js b/frontend/src/hooks/UseAvatarStyle.js new file mode 100644 index 0000000000..334fdc72a9 --- /dev/null +++ b/frontend/src/hooks/UseAvatarStyle.js @@ -0,0 +1,37 @@ +import { useState, useEffect } from 'react'; + +export const useAvatarStyle = (size, editMode, picture) => { + const [sizeClasses, setSizeClasses] = useState('h2 w2 f5'); + const [textPadding, setTextPadding] = useState({}); + const [sizeStyle, setSizeStyle] = useState({}); + const [closeIconStyle, setCloseIconStyle] = useState({ left: '0.4rem' }); + + useEffect(() => { + if (size === 'large') setSizeClasses('h3 w3 f2'); + if (size === 'small') setSizeClasses('f6'); + if (size === 'medium' || !size) setSizeClasses('h2 w2 f5'); + }, [size]); + useEffect(() => { + if (size === 'large') + setTextPadding(editMode ? { top: '-0.5rem' } : { paddingTop: '0.625rem' }); + if (size === 'small') + setTextPadding(editMode ? { top: '-0.5rem' } : { paddingTop: '0.225rem' }); + if (size === 'medium' || !size) + setTextPadding(editMode ? { top: '-0.75rem' } : { paddingTop: '0.375rem' }); + }, [size, editMode]); + useEffect(() => { + if (size === 'large') setCloseIconStyle({ marginLeft: '3rem' }); + if (size === 'small') setCloseIconStyle({ marginLeft: '0' }); + if (size === 'medium' || !size) setCloseIconStyle({ left: '0.4rem' }); + }, [size]); + useEffect(() => { + if (size === 'small') { + const smallStyle = { height: '1.5rem', width: '1.5rem' }; + if (picture) smallStyle.backgroundImage = `url("${picture}")`; + setSizeStyle(smallStyle); + } else { + setSizeStyle(picture ? { backgroundImage: `url("${picture}")` } : {}); + } + }, [picture, size]); + return { sizeClasses, textPadding, closeIconStyle, sizeStyle }; +}; diff --git a/frontend/src/hooks/UseAvatarText.js b/frontend/src/hooks/UseAvatarText.js new file mode 100644 index 0000000000..3ef5943e61 --- /dev/null +++ b/frontend/src/hooks/UseAvatarText.js @@ -0,0 +1,27 @@ +import { useState, useEffect } from 'react'; + +export const useAvatarText = (name, username, number) => { + const [text, setText] = useState(''); + useEffect(() => { + if (name) { + setText( + name + .split(' ') + .map((word) => word[0]) + .join('') + .substr(0, 3), + ); + } else if (number) { + setText(number); + } else { + setText( + username + .split(' ') + .map((word) => word[0]) + .join('') + .substr(0, 3), + ); + } + }, [name, number, username]); + return text; +}; diff --git a/frontend/src/hooks/tests/UseAvatarStyle.test.js b/frontend/src/hooks/tests/UseAvatarStyle.test.js new file mode 100644 index 0000000000..e2775ac9c1 --- /dev/null +++ b/frontend/src/hooks/tests/UseAvatarStyle.test.js @@ -0,0 +1,117 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { useAvatarStyle } from '../UseAvatarStyle'; + +describe('useAvatarStyle sizeClasses', () => { + it('with null size returns medium size', () => { + const { result } = renderHook(() => useAvatarStyle(null, false, null)); + expect(result.current.sizeClasses).toBe('h2 w2 f5'); + }); + it('with medium size returns same as null size', () => { + const { result } = renderHook(() => useAvatarStyle('medium', false, null)); + expect(result.current.sizeClasses).toBe('h2 w2 f5'); + }); + it('with large size returns correct classes', () => { + const { result } = renderHook(() => useAvatarStyle('large', false, null)); + expect(result.current.sizeClasses).toBe('h3 w3 f2'); + }); + it('with small size returns correct classes', () => { + const { result } = renderHook(() => useAvatarStyle('small', false, null)); + expect(result.current.sizeClasses).toBe('f6'); + }); +}); + +describe('useAvatarStyle closeIconStyle', () => { + it('with null size returns medium size', () => { + const { result } = renderHook(() => useAvatarStyle(null, false, null)); + expect(result.current.closeIconStyle).toEqual({ left: '0.4rem' }); + }); + it('with medium size returns same as null size', () => { + const { result } = renderHook(() => useAvatarStyle('medium', false, null)); + expect(result.current.closeIconStyle).toEqual({ left: '0.4rem' }); + }); + it('with large size returns correct classes', () => { + const { result } = renderHook(() => useAvatarStyle('large', false, null)); + expect(result.current.closeIconStyle).toEqual({ marginLeft: '3rem' }); + }); + it('with small size returns correct classes', () => { + const { result } = renderHook(() => useAvatarStyle('small', false, null)); + expect(result.current.closeIconStyle).toEqual({ marginLeft: '0' }); + }); +}); + +describe('useAvatarStyle textPadding', () => { + it('with null size and disabled editMode', () => { + const { result } = renderHook(() => useAvatarStyle(null, false, null)); + expect(result.current.textPadding).toEqual({ paddingTop: '0.375rem' }); + }); + it('with medium size and disabled editMode', () => { + const { result } = renderHook(() => useAvatarStyle('medium', false, null)); + expect(result.current.textPadding).toEqual({ paddingTop: '0.375rem' }); + }); + it('with null size and enabled editMode', () => { + const { result } = renderHook(() => useAvatarStyle(null, true, null)); + expect(result.current.textPadding).toEqual({ top: '-0.75rem' }); + }); + it('with medium size and enabled editMode', () => { + const { result } = renderHook(() => useAvatarStyle('medium', true, null)); + expect(result.current.textPadding).toEqual({ top: '-0.75rem' }); + }); + it('with large size and disabled editMode', () => { + const { result } = renderHook(() => useAvatarStyle('large', false, null)); + expect(result.current.textPadding).toEqual({ paddingTop: '0.625rem' }); + }); + it('with large size and enabled editMode', () => { + const { result } = renderHook(() => useAvatarStyle('large', true, null)); + expect(result.current.textPadding).toEqual({ top: '-0.5rem' }); + }); + it('with small size and disabled editMode', () => { + const { result } = renderHook(() => useAvatarStyle('small', false, null)); + expect(result.current.textPadding).toEqual({ paddingTop: '0.225rem' }); + }); + it('with small size and enabled editMode', () => { + const { result } = renderHook(() => useAvatarStyle('small', true, null)); + expect(result.current.textPadding).toEqual({ top: '-0.5rem' }); + }); +}); + +describe('useAvatarStyle sizeStyle', () => { + it('with null size and null picture returns {}', () => { + const { result } = renderHook(() => useAvatarStyle(null, false, null)); + expect(result.current.sizeStyle).toEqual({}); + }); + it('with medium size and null picture returns {}', () => { + const { result } = renderHook(() => useAvatarStyle('medium', false, null)); + expect(result.current.sizeStyle).toEqual({}); + }); + it('with large size and null picture returns {}', () => { + const { result } = renderHook(() => useAvatarStyle('large', false, null)); + expect(result.current.sizeStyle).toEqual({}); + }); + it('with null size and picture returns backgroundImage', () => { + const { result } = renderHook(() => useAvatarStyle(null, false, 'file.jpg')); + expect(result.current.sizeStyle).toEqual({ backgroundImage: 'url("file.jpg")' }); + }); + it('with medium size and picture returns backgroundImage', () => { + const { result } = renderHook(() => useAvatarStyle('medium', false, 'file.jpg')); + expect(result.current.sizeStyle).toEqual({ backgroundImage: 'url("file.jpg")' }); + }); + it('with large size and picture returns backgroundImage', () => { + const { result } = renderHook(() => useAvatarStyle('large', false, 'file.jpg')); + expect(result.current.sizeStyle).toEqual({ backgroundImage: 'url("file.jpg")' }); + }); + it('with small size and null picture returns only height and width', () => { + const { result } = renderHook(() => useAvatarStyle('small', false, null)); + expect(result.current.sizeStyle).toEqual({ height: '1.5rem', width: '1.5rem' }); + }); + it('with small size and picture returns height, width and backgroundImage', () => { + const { result } = renderHook(() => + useAvatarStyle('small', false, 'https://image.co/file2.jpg'), + ); + expect(result.current.sizeStyle).toEqual({ + height: '1.5rem', + width: '1.5rem', + backgroundImage: 'url("https://image.co/file2.jpg")', + }); + }); +}); diff --git a/frontend/src/hooks/tests/UseAvatarText.test.js b/frontend/src/hooks/tests/UseAvatarText.test.js new file mode 100644 index 0000000000..62d6ede1c4 --- /dev/null +++ b/frontend/src/hooks/tests/UseAvatarText.test.js @@ -0,0 +1,34 @@ +import { renderHook } from '@testing-library/react-hooks'; + +import { useAvatarText } from '../UseAvatarText'; + +describe('useAvatarText sizeClasses', () => { + it('with multiple words name returns the initials', () => { + const { result } = renderHook(() => useAvatarText('long user name')); + expect(result.current).toBe('lun'); + }); + it('with more than 3 words name returns the 3 first initials', () => { + const { result } = renderHook(() => useAvatarText('one two three four five')); + expect(result.current).toBe('ott'); + }); + it('with single word returns the initial', () => { + const { result } = renderHook(() => useAvatarText('Venus')); + expect(result.current).toBe('V'); + }); + it('with multiple words username returns the initials', () => { + const { result } = renderHook(() => useAvatarText(null, 'long user name')); + expect(result.current).toBe('lun'); + }); + it('with more than 3 words username returns the 3 first initials', () => { + const { result } = renderHook(() => useAvatarText(null, 'one two three four five')); + expect(result.current).toBe('ott'); + }); + it('with single word username returns the initial', () => { + const { result } = renderHook(() => useAvatarText(null, 'Venus')); + expect(result.current).toBe('V'); + }); + it('with number returns the number', () => { + const { result } = renderHook(() => useAvatarText(null, null, '+123')); + expect(result.current).toBe('+123'); + }); +});