mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-04 13:48:56 +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
@ -62,6 +62,7 @@
|
|||||||
"react-dnd": "^14.0.2",
|
"react-dnd": "^14.0.2",
|
||||||
"react-dnd-html5-backend": "^14.0.0",
|
"react-dnd-html5-backend": "^14.0.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-outside-click-handler": "^1.3.0",
|
||||||
"react-redux": "^7.2.3",
|
"react-redux": "^7.2.3",
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
@ -71,8 +72,8 @@
|
|||||||
"redux-mock-store": "^1.5.4",
|
"redux-mock-store": "^1.5.4",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"sass": "^1.32.8",
|
"sass": "^1.32.8",
|
||||||
"typescript": "^4.2.3",
|
|
||||||
"swr": "^0.5.5",
|
"swr": "^0.5.5",
|
||||||
|
"typescript": "^4.2.3",
|
||||||
"web-vitals": "^1.0.1"
|
"web-vitals": "^1.0.1"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
@ -20,11 +20,13 @@ export const useCommonStyles = makeStyles(theme => ({
|
|||||||
margin: '1rem 0',
|
margin: '1rem 0',
|
||||||
backgroundColor: theme.palette.division.main,
|
backgroundColor: theme.palette.division.main,
|
||||||
height: '3px',
|
height: '3px',
|
||||||
|
width: '100%',
|
||||||
},
|
},
|
||||||
largeDivider: {
|
largeDivider: {
|
||||||
margin: '2rem 0',
|
margin: '2rem 0',
|
||||||
backgroundColor: theme.palette.division.main,
|
backgroundColor: theme.palette.division.main,
|
||||||
height: '3px',
|
height: '3px',
|
||||||
|
width: '100%',
|
||||||
},
|
},
|
||||||
bold: {
|
bold: {
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
@ -37,6 +39,12 @@ export const useCommonStyles = makeStyles(theme => ({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
},
|
},
|
||||||
|
itemsCenter: {
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
justifyCenter: {
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
flexWrap: {
|
flexWrap: {
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
},
|
},
|
||||||
|
@ -473,10 +473,10 @@ exports[`renders correctly with with variants 1`] = `
|
|||||||
</svg>
|
</svg>
|
||||||
<fieldset
|
<fieldset
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
className="PrivateNotchedOutline-root-16 MuiOutlinedInput-notchedOutline"
|
className="PrivateNotchedOutline-root-18 MuiOutlinedInput-notchedOutline"
|
||||||
>
|
>
|
||||||
<legend
|
<legend
|
||||||
className="PrivateNotchedOutline-legendLabelled-18 PrivateNotchedOutline-legendNotched-19"
|
className="PrivateNotchedOutline-legendLabelled-20 PrivateNotchedOutline-legendNotched-21"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
Stickiness
|
Stickiness
|
||||||
|
@ -140,10 +140,10 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
</svg>
|
</svg>
|
||||||
<fieldset
|
<fieldset
|
||||||
aria-hidden={true}
|
aria-hidden={true}
|
||||||
className="PrivateNotchedOutline-root-14 MuiOutlinedInput-notchedOutline"
|
className="PrivateNotchedOutline-root-16 MuiOutlinedInput-notchedOutline"
|
||||||
>
|
>
|
||||||
<legend
|
<legend
|
||||||
className="PrivateNotchedOutline-legendLabelled-16"
|
className="PrivateNotchedOutline-legendLabelled-18"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
Project
|
Project
|
||||||
@ -175,7 +175,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
aria-disabled={false}
|
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]}
|
onBlur={[Function]}
|
||||||
onDragLeave={[Function]}
|
onDragLeave={[Function]}
|
||||||
onFocus={[Function]}
|
onFocus={[Function]}
|
||||||
@ -194,7 +194,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
checked={false}
|
checked={false}
|
||||||
className="PrivateSwitchBase-input-21 MuiSwitch-input"
|
className="PrivateSwitchBase-input-23 MuiSwitch-input"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@ -318,7 +318,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div
|
<div
|
||||||
className="MuiPaper-root makeStyles-tabNav-22 MuiPaper-elevation1 MuiPaper-rounded"
|
className="MuiPaper-root makeStyles-tabNav-24 MuiPaper-elevation1 MuiPaper-rounded"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="MuiTabs-root"
|
className="MuiTabs-root"
|
||||||
@ -366,7 +366,7 @@ exports[`renders correctly with one feature 1`] = `
|
|||||||
Activation
|
Activation
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
className="PrivateTabIndicator-root-23 PrivateTabIndicator-colorPrimary-24 MuiTabs-indicator"
|
className="PrivateTabIndicator-root-25 PrivateTabIndicator-colorPrimary-26 MuiTabs-indicator"
|
||||||
style={Object {}}
|
style={Object {}}
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
@ -3,12 +3,19 @@ import PropTypes from 'prop-types';
|
|||||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||||
import { useTheme } from '@material-ui/core/styles';
|
import { useTheme } from '@material-ui/core/styles';
|
||||||
import { Route } from 'react-router-dom';
|
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 { DrawerMenu } from '../drawer';
|
||||||
import MenuIcon from '@material-ui/icons/Menu';
|
import MenuIcon from '@material-ui/icons/Menu';
|
||||||
import Breadcrumb from '../breadcrumb';
|
import Breadcrumb from '../breadcrumb';
|
||||||
import ShowUserContainer from '../../user/show-user-container';
|
import UserProfile from '../../user/UserProfile';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import HelpIcon from '@material-ui/icons/Help';
|
||||||
|
|
||||||
import { useStyles } from './styles';
|
import { useStyles } from './styles';
|
||||||
|
|
||||||
@ -31,20 +38,36 @@ const Header = ({ uiConfig, init }) => {
|
|||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<AppBar className={styles.header} position="static">
|
<AppBar className={styles.header} position="static">
|
||||||
<Container className={styles.container}>
|
<Container className={styles.container}>
|
||||||
<IconButton className={styles.drawerButton} onClick={toggleDrawer}>
|
<IconButton
|
||||||
|
className={styles.drawerButton}
|
||||||
|
onClick={toggleDrawer}
|
||||||
|
>
|
||||||
<MenuIcon />
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={!smallScreen}
|
condition={!smallScreen}
|
||||||
show={
|
show={
|
||||||
<Typography variant="h1" className={styles.headerTitle}>
|
<Typography
|
||||||
|
variant="h1"
|
||||||
|
className={styles.headerTitle}
|
||||||
|
>
|
||||||
<Route path="/:path" component={Breadcrumb} />
|
<Route path="/:path" component={Breadcrumb} />
|
||||||
</Typography>
|
</Typography>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.userContainer}>
|
<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>
|
</div>
|
||||||
<DrawerMenu
|
<DrawerMenu
|
||||||
links={links}
|
links={links}
|
||||||
|
@ -23,5 +23,18 @@ export const useStyles = makeStyles(theme => ({
|
|||||||
},
|
},
|
||||||
userContainer: {
|
userContainer: {
|
||||||
marginLeft: 'auto',
|
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 { connect } from 'react-redux';
|
||||||
import ShowUserComponent from './show-user-component';
|
import UserProfile from './UserProfile';
|
||||||
import { fetchUser } from '../../store/user/actions';
|
import { fetchUser, logoutUser } from '../../../store/user/actions';
|
||||||
import { updateSettingForGroup } from '../../store/settings/actions';
|
import { updateSettingForGroup } from '../../../store/settings/actions';
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
fetchUser,
|
fetchUser,
|
||||||
|
logoutUser,
|
||||||
updateSettingLocation: updateSettingForGroup('location'),
|
updateSettingLocation: updateSettingForGroup('location'),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -13,4 +14,4 @@ const mapStateToProps = state => ({
|
|||||||
location: state.settings ? state.settings.toJS().location : {},
|
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',
|
border: '1px solid #f1f1f1',
|
||||||
borderRadius: '3px',
|
borderRadius: '3px',
|
||||||
right: '100px',
|
right: '100px',
|
||||||
color: '#44606e',
|
|
||||||
maxWidth: '350px',
|
maxWidth: '350px',
|
||||||
|
color: '#44606e',
|
||||||
},
|
},
|
||||||
headerContainer: { display: 'flex', padding: '0.5rem' },
|
headerContainer: { display: 'flex', padding: '0.5rem' },
|
||||||
divider: {
|
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;
|
border-radius: 2px;
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
|
z-index: 6000;
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border-radius: 5px;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.showUserSettings {
|
.showUserSettings {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown {
|
.dropdown {
|
||||||
color: #fff;
|
color: #000;
|
||||||
|
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
@ -2406,7 +2406,7 @@ aggregate-error@^3.0.0:
|
|||||||
clean-stack "^2.0.0"
|
clean-stack "^2.0.0"
|
||||||
indent-string "^4.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"
|
version "2.16.0"
|
||||||
resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2"
|
resolved "https://registry.yarnpkg.com/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz#b96274cefa1abb14f623f804173ee97c13971dc2"
|
||||||
integrity sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==
|
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"
|
resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336"
|
||||||
integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==
|
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:
|
constants-browserify@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
|
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
|
||||||
@ -4466,6 +4471,13 @@ doctrine@^3.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
esutils "^2.0.2"
|
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:
|
dom-accessibility-api@^0.5.4:
|
||||||
version "0.5.4"
|
version "0.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.4.tgz#b06d059cdd4a4ad9a79275f9d414a5c126241166"
|
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"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
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:
|
react-redux@^7.2.3:
|
||||||
version "7.2.3"
|
version "7.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.3.tgz#4c084618600bb199012687da9e42123cca3f0be9"
|
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.3.tgz#4c084618600bb199012687da9e42123cca3f0be9"
|
||||||
|
Loading…
Reference in New Issue
Block a user