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');
+ });
+});