mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
Feat/user profile (#274)
* chore: update changelog * feat: user profile * feat: onOutsideClick hook * feat: tune user profile * fix: refactor to button * feat: mobile view * fix: update tests * feat: add link to docs
This commit is contained in:
parent
99c4b7e36d
commit
05334337c2
@ -466,4 +466,4 @@ The latest version of this document is always available in
|
||||
|
||||
## [2.1.0] - 2017-01-20
|
||||
|
||||
- Adjust header #51 #52
|
||||
- Adjust header #51 #52
|
||||
|
@ -62,6 +62,7 @@
|
||||
"react-dnd": "^14.0.2",
|
||||
"react-dnd-html5-backend": "^14.0.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-outside-click-handler": "^1.3.0",
|
||||
"react-redux": "^7.2.3",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "4.0.3",
|
||||
@ -71,8 +72,8 @@
|
||||
"redux-mock-store": "^1.5.4",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"sass": "^1.32.8",
|
||||
"typescript": "^4.2.3",
|
||||
"swr": "^0.5.5",
|
||||
"typescript": "^4.2.3",
|
||||
"web-vitals": "^1.0.1"
|
||||
},
|
||||
"jest": {
|
||||
|
@ -20,11 +20,13 @@ export const useCommonStyles = makeStyles(theme => ({
|
||||
margin: '1rem 0',
|
||||
backgroundColor: theme.palette.division.main,
|
||||
height: '3px',
|
||||
width: '100%',
|
||||
},
|
||||
largeDivider: {
|
||||
margin: '2rem 0',
|
||||
backgroundColor: theme.palette.division.main,
|
||||
height: '3px',
|
||||
width: '100%',
|
||||
},
|
||||
bold: {
|
||||
fontWeight: 'bold',
|
||||
@ -37,6 +39,12 @@ export const useCommonStyles = makeStyles(theme => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
itemsCenter: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
justifyCenter: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
flexWrap: {
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
|
@ -473,10 +473,10 @@ exports[`renders correctly with with variants 1`] = `
|
||||
</svg>
|
||||
<fieldset
|
||||
aria-hidden={true}
|
||||
className="PrivateNotchedOutline-root-16 MuiOutlinedInput-notchedOutline"
|
||||
className="PrivateNotchedOutline-root-18 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
className="PrivateNotchedOutline-legendLabelled-18 PrivateNotchedOutline-legendNotched-19"
|
||||
className="PrivateNotchedOutline-legendLabelled-20 PrivateNotchedOutline-legendNotched-21"
|
||||
>
|
||||
<span>
|
||||
Stickiness
|
||||
|
@ -140,10 +140,10 @@ exports[`renders correctly with one feature 1`] = `
|
||||
</svg>
|
||||
<fieldset
|
||||
aria-hidden={true}
|
||||
className="PrivateNotchedOutline-root-14 MuiOutlinedInput-notchedOutline"
|
||||
className="PrivateNotchedOutline-root-16 MuiOutlinedInput-notchedOutline"
|
||||
>
|
||||
<legend
|
||||
className="PrivateNotchedOutline-legendLabelled-16"
|
||||
className="PrivateNotchedOutline-legendLabelled-18"
|
||||
>
|
||||
<span>
|
||||
Project
|
||||
@ -175,7 +175,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
<span
|
||||
aria-disabled={false}
|
||||
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-18 MuiSwitch-switchBase MuiSwitch-colorSecondary"
|
||||
className="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-20 MuiSwitch-switchBase MuiSwitch-colorSecondary"
|
||||
onBlur={[Function]}
|
||||
onDragLeave={[Function]}
|
||||
onFocus={[Function]}
|
||||
@ -194,7 +194,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
>
|
||||
<input
|
||||
checked={false}
|
||||
className="PrivateSwitchBase-input-21 MuiSwitch-input"
|
||||
className="PrivateSwitchBase-input-23 MuiSwitch-input"
|
||||
disabled={false}
|
||||
onChange={[Function]}
|
||||
type="checkbox"
|
||||
@ -318,7 +318,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
</div>
|
||||
<hr />
|
||||
<div
|
||||
className="MuiPaper-root makeStyles-tabNav-22 MuiPaper-elevation1 MuiPaper-rounded"
|
||||
className="MuiPaper-root makeStyles-tabNav-24 MuiPaper-elevation1 MuiPaper-rounded"
|
||||
>
|
||||
<div
|
||||
className="MuiTabs-root"
|
||||
@ -366,7 +366,7 @@ exports[`renders correctly with one feature 1`] = `
|
||||
Activation
|
||||
</span>
|
||||
<span
|
||||
className="PrivateTabIndicator-root-23 PrivateTabIndicator-colorPrimary-24 MuiTabs-indicator"
|
||||
className="PrivateTabIndicator-root-25 PrivateTabIndicator-colorPrimary-26 MuiTabs-indicator"
|
||||
style={Object {}}
|
||||
/>
|
||||
</button>
|
||||
|
@ -3,12 +3,19 @@ import PropTypes from 'prop-types';
|
||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||
import { useTheme } from '@material-ui/core/styles';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { AppBar, Container, Typography, IconButton } from '@material-ui/core';
|
||||
import {
|
||||
AppBar,
|
||||
Container,
|
||||
Typography,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
} from '@material-ui/core';
|
||||
import { DrawerMenu } from '../drawer';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
import Breadcrumb from '../breadcrumb';
|
||||
import ShowUserContainer from '../../user/show-user-container';
|
||||
import UserProfile from '../../user/UserProfile';
|
||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||
import HelpIcon from '@material-ui/icons/Help';
|
||||
|
||||
import { useStyles } from './styles';
|
||||
|
||||
@ -31,20 +38,36 @@ const Header = ({ uiConfig, init }) => {
|
||||
<React.Fragment>
|
||||
<AppBar className={styles.header} position="static">
|
||||
<Container className={styles.container}>
|
||||
<IconButton className={styles.drawerButton} onClick={toggleDrawer}>
|
||||
<IconButton
|
||||
className={styles.drawerButton}
|
||||
onClick={toggleDrawer}
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<ConditionallyRender
|
||||
condition={!smallScreen}
|
||||
show={
|
||||
<Typography variant="h1" className={styles.headerTitle}>
|
||||
<Typography
|
||||
variant="h1"
|
||||
className={styles.headerTitle}
|
||||
>
|
||||
<Route path="/:path" component={Breadcrumb} />
|
||||
</Typography>
|
||||
}
|
||||
/>
|
||||
|
||||
<div className={styles.userContainer}>
|
||||
<ShowUserContainer />
|
||||
<Tooltip title="Go to the documentation">
|
||||
<a
|
||||
href="https://docs.getunleash.io/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={styles.docsLink}
|
||||
>
|
||||
<HelpIcon className={styles.docsIcon} />
|
||||
</a>
|
||||
</Tooltip>
|
||||
<UserProfile />
|
||||
</div>
|
||||
<DrawerMenu
|
||||
links={links}
|
||||
|
@ -23,5 +23,18 @@ export const useStyles = makeStyles(theme => ({
|
||||
},
|
||||
userContainer: {
|
||||
marginLeft: 'auto',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
docsLink: {
|
||||
color: '#fff',
|
||||
textDecoration: 'none',
|
||||
padding: '0.25rem 0.8rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
docsIcon: {
|
||||
height: '25px',
|
||||
width: '25px',
|
||||
},
|
||||
}));
|
||||
|
@ -0,0 +1,21 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
container: {
|
||||
width: '100%',
|
||||
transform: 'translateY(-30px)',
|
||||
},
|
||||
form: {
|
||||
width: '100%',
|
||||
'& > *': {
|
||||
width: '100%',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
width: '150px',
|
||||
marginTop: '1.15rem',
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
width: '100px',
|
||||
},
|
||||
},
|
||||
}));
|
@ -0,0 +1,163 @@
|
||||
import { SyntheticEvent, useState } from 'react';
|
||||
import { Button, TextField, Typography } from '@material-ui/core';
|
||||
import classnames from 'classnames';
|
||||
import { useStyles } from './EditProfile.styles';
|
||||
import { useCommonStyles } from '../../../../common.styles';
|
||||
import PasswordChecker from '../../common/ResetPasswordForm/PasswordChecker/PasswordChecker';
|
||||
import PasswordMatcher from '../../common/ResetPasswordForm/PasswordMatcher/PasswordMatcher';
|
||||
import { headers } from '../../../../store/api-helper';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||
import useLoading from '../../../../hooks/useLoading';
|
||||
import {
|
||||
BAD_REQUEST,
|
||||
NOT_FOUND,
|
||||
OK,
|
||||
UNAUTHORIZED,
|
||||
} from '../../../../constants/statusCodes';
|
||||
|
||||
interface IEditProfileProps {
|
||||
setEditingProfile: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setUpdatedPassword: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const EditProfile = ({
|
||||
setEditingProfile,
|
||||
setUpdatedPassword,
|
||||
}: IEditProfileProps) => {
|
||||
const styles = useStyles();
|
||||
const commonStyles = useCommonStyles();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [validPassword, setValidPassword] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const ref = useLoading(loading);
|
||||
|
||||
const submit = async (e: SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validPassword || password !== confirmPassword) {
|
||||
setError(
|
||||
'Password is not valid, or your passwords do not match. Please provide a password with length over 10 characters, an uppercase letter, a lowercase letter, a number and a symbol.'
|
||||
);
|
||||
} else {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
try {
|
||||
const res = await fetch('api/admin/user/change-password', {
|
||||
headers,
|
||||
body: JSON.stringify({ password, confirmPassword }),
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
});
|
||||
handleResponse(res);
|
||||
} catch (e) {
|
||||
setError(e);
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleResponse = (res: Response) => {
|
||||
if (res.status === BAD_REQUEST) {
|
||||
setError(
|
||||
'Password could not be accepted. Please make sure you are inputting a valid password.'
|
||||
);
|
||||
}
|
||||
|
||||
if (res.status === UNAUTHORIZED) {
|
||||
setError('You are not authorized to make this request.');
|
||||
}
|
||||
|
||||
if (res.status === NOT_FOUND) {
|
||||
setError(
|
||||
'The resource you requested could not be found on the server.'
|
||||
);
|
||||
}
|
||||
|
||||
if (res.status === OK) {
|
||||
setEditingProfile(false);
|
||||
setUpdatedPassword(true);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container} ref={ref}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
style={{ fontWeight: 'bold' }}
|
||||
data-loading
|
||||
>
|
||||
Update password
|
||||
</Typography>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(error)}
|
||||
show={
|
||||
<Alert data-loading severity="error">
|
||||
{error}
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
<form
|
||||
className={classnames(
|
||||
styles.form,
|
||||
commonStyles.contentSpacingY
|
||||
)}
|
||||
>
|
||||
<PasswordChecker
|
||||
password={password}
|
||||
callback={setValidPassword}
|
||||
data-loading
|
||||
/>
|
||||
<TextField
|
||||
data-loading
|
||||
variant="outlined"
|
||||
size="small"
|
||||
label="Password"
|
||||
type="password"
|
||||
name="password"
|
||||
value={password}
|
||||
autoComplete="on"
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
data-loading
|
||||
variant="outlined"
|
||||
size="small"
|
||||
label="Confirm password"
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
value={confirmPassword}
|
||||
autoComplete="on"
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
<PasswordMatcher
|
||||
data-loading
|
||||
started={Boolean(password && confirmPassword)}
|
||||
matchingPasswords={password === confirmPassword}
|
||||
/>
|
||||
<Button
|
||||
data-loading
|
||||
variant="contained"
|
||||
color="primary"
|
||||
className={styles.button}
|
||||
type="submit"
|
||||
onClick={submit}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
data-loading
|
||||
className={styles.button}
|
||||
type="submit"
|
||||
onClick={() => setEditingProfile(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditProfile;
|
93
frontend/src/component/user/UserProfile/UserProfile.jsx
Normal file
93
frontend/src/component/user/UserProfile/UserProfile.jsx
Normal file
@ -0,0 +1,93 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import OutsideClickHandler from 'react-outside-click-handler';
|
||||
|
||||
import { Avatar, Button } from '@material-ui/core';
|
||||
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
|
||||
import { useStyles } from './UserProfile.styles';
|
||||
import { useCommonStyles } from '../../../common.styles';
|
||||
import UserProfileContent from './UserProfileContent/UserProfileContent';
|
||||
|
||||
const UserProfile = ({
|
||||
profile,
|
||||
location,
|
||||
fetchUser,
|
||||
updateSettingLocation,
|
||||
logoutUser,
|
||||
}) => {
|
||||
const [showProfile, setShowProfile] = useState(false);
|
||||
|
||||
const styles = useStyles();
|
||||
const commonStyles = useCommonStyles();
|
||||
|
||||
const [possibleLocales, setPossibleLocales] = useState([
|
||||
{ value: 'en-US', image: 'en-US' },
|
||||
{ value: 'en-GB', image: 'en-GB' },
|
||||
{ value: 'nb-NO', image: 'nb-NO' },
|
||||
{ value: 'sv-SE', image: 'sv-SE' },
|
||||
{ value: 'da-DK', image: 'da-DK' },
|
||||
{ value: 'en-IN', image: 'en-IN' },
|
||||
{ value: 'de', image: 'de_DE' },
|
||||
{ value: 'cs', image: 'cs_CZ' },
|
||||
{ value: 'pt-BR', image: 'pt_BR' },
|
||||
{ value: 'fr-FR', image: 'fr-FR' },
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUser();
|
||||
|
||||
const locale = navigator.language || navigator.userLanguage;
|
||||
let found = possibleLocales.find(l => l.value === locale);
|
||||
if (!found) {
|
||||
setPossibleLocales(prev => ({
|
||||
...prev,
|
||||
value: locale,
|
||||
image: 'unknown-locale',
|
||||
}));
|
||||
}
|
||||
/* eslint-disable-next-line*/
|
||||
}, []);
|
||||
|
||||
const email = profile ? profile.email : '';
|
||||
const imageUrl = email ? profile.imageUrl : 'unknown-user.png';
|
||||
|
||||
return (
|
||||
<OutsideClickHandler onOutsideClick={() => setShowProfile(false)}>
|
||||
<div className={styles.profileContainer}>
|
||||
<Button
|
||||
className={classnames(
|
||||
commonStyles.flexRow,
|
||||
commonStyles.itemsCenter,
|
||||
styles.button
|
||||
)}
|
||||
onClick={() => setShowProfile(prev => !prev)}
|
||||
tabIndex="1"
|
||||
role="button"
|
||||
disableRipple
|
||||
>
|
||||
<Avatar alt="user image" src={imageUrl} />
|
||||
<KeyboardArrowDownIcon />
|
||||
</Button>
|
||||
<UserProfileContent
|
||||
showProfile={showProfile}
|
||||
imageUrl={imageUrl}
|
||||
profile={profile}
|
||||
updateSettingLocation={updateSettingLocation}
|
||||
possibleLocales={possibleLocales}
|
||||
logoutUser={logoutUser}
|
||||
location={location}
|
||||
/>
|
||||
</div>
|
||||
</OutsideClickHandler>
|
||||
);
|
||||
};
|
||||
|
||||
UserProfile.propTypes = {
|
||||
profile: PropTypes.object,
|
||||
location: PropTypes.object,
|
||||
fetchUser: PropTypes.func.isRequired,
|
||||
updateSettingLocation: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default UserProfile;
|
@ -0,0 +1,14 @@
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
|
||||
export const useStyles = makeStyles({
|
||||
userProfileMenu: {
|
||||
display: 'flex',
|
||||
},
|
||||
profileContainer: {
|
||||
position: 'relative',
|
||||
},
|
||||
button: {
|
||||
color: '#fff',
|
||||
padding: '0.2rem 1rem',
|
||||
},
|
||||
});
|
@ -0,0 +1,154 @@
|
||||
import { useState } from 'react';
|
||||
import ConditionallyRender from '../../../common/ConditionallyRender';
|
||||
import {
|
||||
Paper,
|
||||
Avatar,
|
||||
Typography,
|
||||
Button,
|
||||
FormControl,
|
||||
Select,
|
||||
InputLabel,
|
||||
} from '@material-ui/core';
|
||||
import classnames from 'classnames';
|
||||
import { useStyles } from './UserProfileContent.styles';
|
||||
import { useCommonStyles } from '../../../../common.styles';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import EditProfile from '../EditProfile/EditProfile';
|
||||
import legacyStyles from '../../user.module.scss';
|
||||
|
||||
const UserProfileContent = ({
|
||||
showProfile,
|
||||
profile,
|
||||
possibleLocales,
|
||||
updateSettingLocation,
|
||||
imageUrl,
|
||||
location,
|
||||
logoutUser,
|
||||
}) => {
|
||||
const commonStyles = useCommonStyles();
|
||||
const [currentLocale, setCurrentLocale] = useState(location.locale);
|
||||
const [updatedPassword, setUpdatedPassword] = useState(false);
|
||||
const [edititingProfile, setEditingProfile] = useState(false);
|
||||
const styles = useStyles();
|
||||
|
||||
const setLocale = value => {
|
||||
updateSettingLocation('locale', value);
|
||||
};
|
||||
|
||||
const profileAvatarClasses = classnames(styles.avatar, {
|
||||
[styles.editingAvatar]: edititingProfile,
|
||||
});
|
||||
|
||||
const profileEmailClasses = classnames(styles.profileEmail, {
|
||||
[styles.editingEmail]: edititingProfile,
|
||||
});
|
||||
|
||||
const handleChange = e => {
|
||||
const { value } = e.target;
|
||||
setCurrentLocale(value);
|
||||
setLocale(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<ConditionallyRender
|
||||
condition={showProfile}
|
||||
show={
|
||||
<Paper
|
||||
className={classnames(
|
||||
styles.profile,
|
||||
commonStyles.flexColumn,
|
||||
commonStyles.itemsCenter,
|
||||
commonStyles.contentSpacingY
|
||||
)}
|
||||
>
|
||||
<Avatar
|
||||
alt="user image"
|
||||
src={imageUrl}
|
||||
className={profileAvatarClasses}
|
||||
/>
|
||||
<Typography variant="body1" className={profileEmailClasses}>
|
||||
{profile?.email}
|
||||
</Typography>
|
||||
<ConditionallyRender
|
||||
condition={updatedPassword}
|
||||
show={
|
||||
<Alert onClose={() => setUpdatedPassword(false)}>
|
||||
Successfully updated password.
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={!edititingProfile}
|
||||
show={
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={() => setEditingProfile(true)}
|
||||
>
|
||||
Update password
|
||||
</Button>
|
||||
<div className={commonStyles.divider} />
|
||||
<div className={legacyStyles.showUserSettings}>
|
||||
<FormControl
|
||||
variant="outlined"
|
||||
size="small"
|
||||
style={{
|
||||
width: '100%',
|
||||
minWidth: '120px',
|
||||
}}
|
||||
>
|
||||
<InputLabel
|
||||
htmlFor="locale-select"
|
||||
style={{ backgroundColor: '#fff' }}
|
||||
>
|
||||
Date/Time formatting
|
||||
</InputLabel>
|
||||
|
||||
<Select
|
||||
id="locale-select"
|
||||
native
|
||||
value={currentLocale || ''}
|
||||
onChange={handleChange}
|
||||
MenuProps={{
|
||||
style: {
|
||||
zIndex: 9999,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{possibleLocales.map(locale => {
|
||||
return (
|
||||
<option
|
||||
key={locale.value}
|
||||
value={locale.value}
|
||||
>
|
||||
{locale.value}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className={commonStyles.divider} />
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={logoutUser}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
elseShow={
|
||||
<EditProfile
|
||||
setEditingProfile={setEditingProfile}
|
||||
setUpdatedPassword={setUpdatedPassword}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Paper>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserProfileContent;
|
@ -0,0 +1,29 @@
|
||||
import { makeStyles } from '@material-ui/styles';
|
||||
|
||||
export const useStyles = makeStyles(theme => ({
|
||||
profile: {
|
||||
position: 'absolute',
|
||||
zIndex: '5000',
|
||||
minWidth: '300px',
|
||||
right: 0,
|
||||
padding: '1.5rem',
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
width: '100%',
|
||||
padding: '1rem',
|
||||
},
|
||||
},
|
||||
avatar: {
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
transition: 'transform 0.4s ease',
|
||||
},
|
||||
editingAvatar: {
|
||||
transform: 'translateX(-102px) translateY(-9px)',
|
||||
},
|
||||
profileEmail: {
|
||||
transition: 'transform 0.4s ease',
|
||||
},
|
||||
editingEmail: {
|
||||
transform: 'translateX(10px) translateY(-60px)',
|
||||
},
|
||||
}));
|
@ -1,10 +1,11 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ShowUserComponent from './show-user-component';
|
||||
import { fetchUser } from '../../store/user/actions';
|
||||
import { updateSettingForGroup } from '../../store/settings/actions';
|
||||
import UserProfile from './UserProfile';
|
||||
import { fetchUser, logoutUser } from '../../../store/user/actions';
|
||||
import { updateSettingForGroup } from '../../../store/settings/actions';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchUser,
|
||||
logoutUser,
|
||||
updateSettingLocation: updateSettingForGroup('location'),
|
||||
};
|
||||
|
||||
@ -13,4 +14,4 @@ const mapStateToProps = state => ({
|
||||
location: state.settings ? state.settings.toJS().location : {},
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ShowUserComponent);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(UserProfile);
|
@ -5,8 +5,8 @@ export const useStyles = makeStyles(theme => ({
|
||||
border: '1px solid #f1f1f1',
|
||||
borderRadius: '3px',
|
||||
right: '100px',
|
||||
color: '#44606e',
|
||||
maxWidth: '350px',
|
||||
color: '#44606e',
|
||||
},
|
||||
headerContainer: { display: 'flex', padding: '0.5rem' },
|
||||
divider: {
|
||||
|
@ -1,104 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './user.module.scss';
|
||||
import { MenuItem, Avatar, Typography, Icon } from '@material-ui/core';
|
||||
import DropdownMenu from '../common/DropdownMenu/DropdownMenu';
|
||||
|
||||
export default class ShowUserComponent extends React.Component {
|
||||
static propTypes = {
|
||||
profile: PropTypes.object,
|
||||
location: PropTypes.object,
|
||||
fetchUser: PropTypes.func.isRequired,
|
||||
updateSettingLocation: PropTypes.func.isRequired,
|
||||
};
|
||||
possibleLocales = [
|
||||
{ value: 'en-US', image: 'en-US' },
|
||||
{ value: 'en-GB', image: 'en-GB' },
|
||||
{ value: 'nb-NO', image: 'nb-NO' },
|
||||
{ value: 'sv-SE', image: 'sv-SE' },
|
||||
{ value: 'da-DK', image: 'da-DK' },
|
||||
{ value: 'en-IN', image: 'en-IN' },
|
||||
{ value: 'de', image: 'de_DE' },
|
||||
{ value: 'cs', image: 'cs_CZ' },
|
||||
{ value: 'pt-BR', image: 'pt_BR' },
|
||||
{ value: 'fr-FR', image: 'fr-FR' },
|
||||
];
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchUser();
|
||||
|
||||
// find default locale and add it in choices if not present
|
||||
const locale = navigator.language || navigator.userLanguage;
|
||||
let found = this.possibleLocales.find(l => l.value === locale);
|
||||
if (!found) {
|
||||
this.possibleLocales.push({
|
||||
value: locale,
|
||||
image: 'unknown-locale',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getLocale() {
|
||||
return (
|
||||
(this.props.location && this.props.location.locale) ||
|
||||
navigator.language ||
|
||||
navigator.userLanguage
|
||||
);
|
||||
}
|
||||
|
||||
setLocale(locale) {
|
||||
this.props.updateSettingLocation('locale', locale.value);
|
||||
}
|
||||
|
||||
render() {
|
||||
const email = this.props.profile ? this.props.profile.email : '';
|
||||
const locale = this.getLocale();
|
||||
let foundLocale = this.possibleLocales.find(l => l.value === locale);
|
||||
const imageUrl = email
|
||||
? this.props.profile.imageUrl
|
||||
: 'unknown-user.png';
|
||||
const imageLocale = foundLocale
|
||||
? `${foundLocale.image}.png`
|
||||
: `unknown-locale.png`;
|
||||
return (
|
||||
<div className={styles.showUserSettings}>
|
||||
<DropdownMenu
|
||||
className={styles.dropdown}
|
||||
startIcon={
|
||||
<Icon
|
||||
component={'img'}
|
||||
alt={locale}
|
||||
src={imageLocale}
|
||||
className={styles.labelFlag}
|
||||
/>
|
||||
}
|
||||
renderOptions={() =>
|
||||
this.possibleLocales.map(i => (
|
||||
<MenuItem
|
||||
key={i.value}
|
||||
onClick={() => this.setLocale(i)}
|
||||
>
|
||||
<div className={styles.showLocale}>
|
||||
<img
|
||||
src={`${i.image}.png`}
|
||||
title={i.value}
|
||||
alt={i.value}
|
||||
/>
|
||||
<Typography variant="p">
|
||||
{i.value}
|
||||
</Typography>
|
||||
</div>
|
||||
</MenuItem>
|
||||
))
|
||||
}
|
||||
label="Locale"
|
||||
/>
|
||||
<Avatar
|
||||
alt="user image"
|
||||
src={imageUrl}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -14,22 +14,23 @@
|
||||
border-radius: 2px;
|
||||
width: 30px;
|
||||
height: 18px;
|
||||
z-index: 6000;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 5px;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.showUserSettings {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
color: #fff;
|
||||
color: #000;
|
||||
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
@ -251,4 +251,4 @@ UsersList.propTypes = {
|
||||
location: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default UsersList;
|
||||
export default UsersList;
|
@ -2406,7 +2406,7 @@ aggregate-error@^3.0.0:
|
||||
clean-stack "^2.0.0"
|
||||
indent-string "^4.0.0"
|
||||
|
||||
airbnb-prop-types@^2.16.0:
|
||||
airbnb-prop-types@^2.15.0, airbnb-prop-types@^2.16.0:
|
||||
version "2.16.0"
|
||||
resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2"
|
||||
integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==
|
||||
@ -3720,6 +3720,11 @@ console-browserify@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
|
||||
integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
|
||||
|
||||
"consolidated-events@^1.1.1 || ^2.0.0":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/consolidated-events/-/consolidated-events-2.0.2.tgz#da8d8f8c2b232831413d9e190dc11669c79f4a91"
|
||||
integrity sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ==
|
||||
|
||||
constants-browserify@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
|
||||
@ -4466,6 +4471,13 @@ doctrine@^3.0.0:
|
||||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
document.contains@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/document.contains/-/document.contains-1.0.2.tgz#4260abad67a6ae9e135c1be83d68da0db169d5f0"
|
||||
integrity sha512-YcvYFs15mX8m3AO1QNQy3BlIpSMfNRj3Ujk2BEJxsZG+HZf7/hZ6jr7mDpXrF8q+ff95Vef5yjhiZxm8CGJr6Q==
|
||||
dependencies:
|
||||
define-properties "^1.1.3"
|
||||
|
||||
dom-accessibility-api@^0.5.4:
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz#b06d059cdd4a4ad9a79275f9d414a5c126241166"
|
||||
@ -9852,6 +9864,17 @@ react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
||||
react-outside-click-handler@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/react-outside-click-handler/-/react-outside-click-handler-1.3.0.tgz#3831d541ac059deecd38ec5423f81e80ad60e115"
|
||||
integrity sha512-Te/7zFU0oHpAnctl//pP3hEAeobfeHMyygHB8MnjP6sX5OR8KHT1G3jmLsV3U9RnIYo+Yn+peJYWu+D5tUS8qQ==
|
||||
dependencies:
|
||||
airbnb-prop-types "^2.15.0"
|
||||
consolidated-events "^1.1.1 || ^2.0.0"
|
||||
document.contains "^1.0.1"
|
||||
object.values "^1.1.0"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
react-redux@^7.2.3:
|
||||
version "7.2.3"
|
||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.3.tgz#4c084618600bb199012687da9e42123cca3f0be9"
|
||||
|
Loading…
Reference in New Issue
Block a user