User profile page

master
Tudor Stanciu 2023-03-20 18:49:35 +02:00
parent 7cd40357ab
commit b617d59b69
16 changed files with 271 additions and 200 deletions

4
.env
View File

@ -1,6 +1,6 @@
#REACT_APP_TUITIO_URL=http://localhost:5063/identity/authenticate?UserName={username}&Password={password}
#REACT_APP_TUITIO_URL=http://localhost:5063
REACT_APP_TUITIO_URL=https://lab.code-rove.com/tuitio
#REACT_APP_NETWORK_RESURRECTOR_API_URL=http://localhost:5064
REACT_APP_NETWORK_RESURRECTOR_API_URL=https://lab.code-rove.com/network-resurrector-api

View File

@ -12,9 +12,16 @@
"English": "English",
"Romanian": "Romanian"
},
"Generic": {
"Copy": "Copy",
"OpenInNewTab": "Open in new tab",
"CopiedToClipboard": "Copied to clipboard",
"SendEmail": "Send email"
},
"Menu": {
"Dashboard": "Dashboard",
"Machines": "Machines",
"System": "System",
"Trash": "Trash",
"Settings": "Settings"
},
@ -22,12 +29,15 @@
"Username": "Username",
"Password": "Password",
"Label": "Login",
"ChangeUser": "Change user",
"UserChanged": "User changed",
"Logout": "Logout",
"IncorrectCredentials": "Incorrect credentials.",
"Hello": "Hi, {{username}}",
"AuthenticationDate": "Authentication date"
"IncorrectCredentials": "Incorrect credentials."
},
"User": {
"Profile": {
"Hello": "Hi, {{userName}}",
"Description": "{{userName}}, authenticated on {{loginDate}}",
"OpenPortfolio": "Open portfolio"
}
},
"Machine": {
"FullName": "Full machine name",

View File

@ -3,9 +3,16 @@
"English": "Engleză",
"Romanian": "Română"
},
"Generic": {
"Copy": "Copiază",
"OpenInNewTab": "Deschide într-un tab nou",
"CopiedToClipboard": "Copiat în clipboard",
"SendEmail": "Trimite email"
},
"Menu": {
"Dashboard": "Bord",
"Machines": "Mașini",
"System": "Sistem",
"Trash": "Gunoi",
"Settings": "Setări"
},
@ -13,12 +20,15 @@
"Username": "Utilizator",
"Password": "Parolă",
"Label": "Autentificare",
"ChangeUser": "Schimbă utilizatorul",
"UserChanged": "Utilizator schimbat",
"Logout": "Deconectare",
"IncorrectCredentials": "Credențiale incorecte.",
"Hello": "Salut, {{username}}",
"AuthenticationDate": "Momentul autentificării"
"IncorrectCredentials": "Credențiale incorecte."
},
"User": {
"Profile": {
"Hello": "Salut, {{userName}}",
"Description": "{{userName}}, autentificat pe {{loginDate}}",
"OpenPortfolio": "Deschide portofoliu"
}
},
"Machine": {
"FullName": "Nume intreg masina",

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

@ -1,16 +1,16 @@
import React from "react";
import { Route, Switch } from "react-router-dom";
import PageNotFound from "./PageNotFound";
import LoginContainer from "../../features/login/components/LoginContainer";
import NetworkContainer from "../../features/network/components/NetworkContainer";
import SettingsContainer from "../../features/settings/components/SettingsContainer";
import DashboardContainer from "../../features/dashboard/components/DashboardContainer";
import UserProfileContainer from "../../features/user/profile/components/UserProfileContainer";
const AppRoutes = () => {
return (
<Switch>
<Route exact path="/dashboard" component={DashboardContainer} />
<Route exact path="/user-profile" component={LoginContainer} />
<Route exact path="/user-profile" component={UserProfileContainer} />
<Route exact path="/machines" component={NetworkContainer} />
<Route exact path="/settings" component={SettingsContainer} />
<Route component={PageNotFound} />

View File

@ -15,6 +15,7 @@ import ChevronRightIcon from "@material-ui/icons/ChevronRight";
import ListItem from "@material-ui/core/ListItem";
import DeleteIcon from "@material-ui/icons/Delete";
import DnsIcon from "@material-ui/icons/Dns";
import DeviceHubIcon from "@material-ui/icons/DeviceHub";
import SettingsIcon from "@material-ui/icons/Settings";
import DashboardIcon from "@material-ui/icons/Dashboard";
import { useHistory } from "react-router-dom";
@ -74,6 +75,12 @@ const Sidebar = ({ open, handleDrawerClose }) => {
</ListItemIcon>
<ListItemText primary={t("Menu.Machines")} />
</ListItem>
<ListItem button key="system" onClick={() => history.push("/system")}>
<ListItemIcon>
<DeviceHubIcon />
</ListItemIcon>
<ListItemText primary={t("Menu.System")} />
</ListItem>
</List>
<Divider />
<List>

View File

@ -1,111 +0,0 @@
import React, { useState, useMemo } from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import {
Avatar,
Collapse,
Tooltip,
Divider,
Typography,
Card,
CardContent,
CardActions,
CardHeader,
IconButton
} from "@material-ui/core";
import { AccountBox, RotateLeft, ExitToApp } from "@material-ui/icons";
import LoginComponent from "./LoginComponent";
import { useTranslation } from "react-i18next";
import { useToast } from "../../../hooks";
import styles from "../styles";
import { useTuitioUser } from "@flare/tuitio-client-react";
const useStyles = makeStyles(styles);
const LoggedInComponent = ({ credentials, onChange, onLogin, onLogout }) => {
const classes = useStyles();
const { t } = useTranslation();
const [expanded, setExpanded] = useState(false);
const { info } = useToast();
const { userName } = useTuitioUser();
const handleExpandLogin = () => {
setExpanded(!expanded);
};
const loginDate = useMemo(() => {
const valueForDisplay = t("LONG_DATE", { date: new Date() });
return valueForDisplay;
}, [t]);
const handleLogin = async () => {
const result = await onLogin();
if (result) {
setExpanded(false);
info(t("Login.UserChanged"));
}
};
return (
<div className={classes.loggedInContent}>
<Card className={classes.loggedInCard}>
<CardHeader
avatar={
<Avatar aria-label="user" className={classes.avatar}>
<AccountBox />
</Avatar>
}
title={<strong>{t("Login.Hello", { username: userName })}</strong>}
subheader={
<Tooltip title={t("Login.AuthenticationDate")}>
<Typography variant="caption" display="block">
{loginDate}
</Typography>
</Tooltip>
}
/>
<CardActions disableSpacing>
<Tooltip title={t("Login.ChangeUser")}>
<IconButton
size="small"
className={classes.onRight}
onClick={handleExpandLogin}
aria-expanded={expanded}
aria-label="show login component"
>
<RotateLeft />
</IconButton>
</Tooltip>
<Tooltip title={t("Login.Logout")}>
<IconButton size="small" onClick={onLogout}>
<ExitToApp />
</IconButton>
</Tooltip>
</CardActions>
<Divider />
<Collapse in={expanded} timeout="auto" unmountOnExit>
<CardContent
className={classes.collapseContent}
style={{ paddingBottom: "5px" }}
>
<LoginComponent
credentials={credentials}
onChange={onChange}
onLogin={handleLogin}
/>
</CardContent>
</Collapse>
</Card>
</div>
);
};
LoggedInComponent.propTypes = {
credentials: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
onLogin: PropTypes.func.isRequired,
onLogout: PropTypes.func.isRequired
};
export default LoggedInComponent;

View File

@ -2,8 +2,7 @@ import React, { useState } from "react";
import LoginCard from "./LoginCard";
import { useToast } from "../../../hooks";
import { useTranslation } from "react-i18next";
import LoggedInComponent from "./LoggedInComponent";
import { useTuitioClient, useTuitioToken } from "@flare/tuitio-client-react";
import { useTuitioClient } from "@flare/tuitio-client-react";
const LoginContainer = () => {
const [credentials, setCredentials] = useState({
@ -13,11 +12,10 @@ const LoginContainer = () => {
const { error } = useToast();
const { t } = useTranslation();
const { login, logout } = useTuitioClient({
const { login } = useTuitioClient({
onLoginFailed: () => error(t("Login.IncorrectCredentials")),
onLoginError: err => error(err.message)
});
const { valid: tokenIsValid } = useTuitioToken();
const handleChange = prop => event => {
setCredentials(prev => ({ ...prev, [prop]: event.target.value }));
@ -28,27 +26,12 @@ const LoginContainer = () => {
return login(userName, password);
};
const handleLogout = () => {
logout();
};
return (
<>
{tokenIsValid ? (
<LoggedInComponent
credentials={credentials}
onChange={handleChange}
onLogin={handleLogin}
onLogout={handleLogout}
/>
) : (
<LoginCard
credentials={credentials}
onChange={handleChange}
onLogin={handleLogin}
/>
)}
</>
);
};

View File

@ -1,23 +1,7 @@
const styles = theme => ({
loggedInCard: {
minWidth: 350
},
onRight: {
marginLeft: "auto"
},
avatar: {
backgroundColor: theme.palette.primary.main
},
collapseContent: {
padding: 0
},
loggedInContent: {
minHeight: "80vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "calc(10px + 2vmin)"
},
appLogin: {
minHeight: "100vh",
display: "flex",

View File

@ -1,6 +1,5 @@
import React from "react";
import MachinesContainer from "../../machines/components/MachinesContainer";
//import NotesContainer from "../../notes/components/NotesContainer";
import { makeStyles } from "@material-ui/core/styles";
import styles from "../styles";
@ -12,7 +11,6 @@ const NetworkContainer = () => {
return (
<div className={classes.root}>
<MachinesContainer />
{/* <NotesContainer /> */}
</div>
);
};

View File

@ -1,33 +0,0 @@
import React, { useContext } from "react";
import { TextField, Button } from "@material-ui/core";
import {
ApplicationStateContext,
ApplicationDispatchContext
} from "../../../state/ApplicationContexts";
const NotesContainer = () => {
const state = useContext(ApplicationStateContext);
const dispatchActions = useContext(ApplicationDispatchContext);
const handleChange = prop => event => {
dispatchActions.onNetworkChange(prop, event.target.value);
};
return (
<>
<TextField
id="ertet"
label="Test"
onChange={handleChange("test")}
value={state.network.test}
/>
<br />
<br />
<Button variant="contained" color="primary">
Read machines
</Button>
</>
);
};
export default NotesContainer;

View File

@ -0,0 +1,112 @@
import React from "react";
import PropTypes from "prop-types";
import {
Grid,
List,
ListItem,
ListItemText,
ListItemIcon,
Link,
IconButton,
Tooltip
} from "@material-ui/core";
import UserProfilePicture from "./UserProfilePicture";
import BusinessCenterIcon from "@material-ui/icons/BusinessCenter";
import { FileCopyOutlined } from "@material-ui/icons";
import EmailIcon from "@material-ui/icons/Email";
import { useToast } from "../../../../hooks";
import { useTranslation } from "react-i18next";
const UserProfileCardContent = ({ userData }) => {
const { email, profilePictureUrl } = userData;
const { t } = useTranslation();
const { info } = useToast();
const handleCopyToClipboard = url => () => {
navigator.clipboard.writeText(url);
info(t("Generic.CopiedToClipboard"));
};
const handleEmailSending = event => {
window.location.href = `mailto:${email}`;
event.preventDefault();
};
const userName = `${userData.firstName} ${userData.lastName}`;
return (
<Grid container spacing={2}>
<Grid item xs={12} sm={4} lg={2}>
<UserProfilePicture userData={userData} />
</Grid>
<Grid item xs={12} sm={8} lg={10}>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<List>
<ListItem dense>
<ListItemIcon>
<BusinessCenterIcon />
</ListItemIcon>
<ListItemText
primary={
<Tooltip title={t("User.Profile.OpenPortfolio")}>
<Link
href="https://lab.code-rove.com/tsp/"
target="_blank"
>
{userName}
</Link>
</Tooltip>
}
/>
</ListItem>
<ListItem dense>
<ListItemIcon>
<EmailIcon />
</ListItemIcon>
<ListItemText
primary={
<Tooltip title={t("Generic.SendEmail")}>
<Link href="#" onClick={handleEmailSending}>
{email}
</Link>
</Tooltip>
}
/>
</ListItem>
{profilePictureUrl && (
<ListItem dense>
<ListItemIcon>
<Tooltip title={t("Generic.Copy")}>
<IconButton
size="small"
onClick={handleCopyToClipboard(profilePictureUrl)}
>
<FileCopyOutlined />
</IconButton>
</Tooltip>
</ListItemIcon>
<ListItemText
primary={
<Tooltip title={t("Generic.OpenInNewTab")}>
<Link href={profilePictureUrl} target="_blank">
{profilePictureUrl}
</Link>
</Tooltip>
}
/>
</ListItem>
)}
</List>
</Grid>
</Grid>
</Grid>
</Grid>
);
};
UserProfileCardContent.propTypes = {
userData: PropTypes.object.isRequired
};
export default UserProfileCardContent;

View File

@ -0,0 +1,47 @@
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Card, CardHeader, CardContent } from "@material-ui/core";
import { useTuitioToken } from "@flare/tuitio-client-react";
import { camelizeKeys } from "../../../../utils/camelizeKeys";
import PageTitle from "../../../../components/common/PageTitle";
import UserProfileCardContent from "./UserProfileCardContent";
const UserProfileContainer = () => {
const { t } = useTranslation();
const { token } = useTuitioToken();
const decodedToken = useMemo(() => atob(token), [token]);
const userData = useMemo(
() => camelizeKeys(JSON.parse(decodedToken)),
[decodedToken]
);
console.log("userData", userData);
const userLoginDate = useMemo(
() =>
t("DATE_FORMAT", {
date: { value: userData.createdAt, format: "DD-MM-YYYY HH:mm:ss" }
}),
[t, userData.createdAt]
);
const userDescription = t("User.Profile.Description", {
userName: `${userData.firstName} ${userData.lastName}`,
loginDate: userLoginDate
});
return (
<>
<PageTitle
text={t("User.Profile.Hello", { userName: userData.firstName })}
/>
<Card>
<CardHeader title={userData.userName} subheader={userDescription} />
<CardContent>
<UserProfileCardContent userData={userData} />
</CardContent>
</Card>
</>
);
};
export default UserProfileContainer;

View File

@ -0,0 +1,21 @@
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import style from "../styles";
import Avatar from "@material-ui/core/Avatar";
import DefaultUserProfilePicture from "../../../../assets/images/DefaultUserProfilePicture.png";
const useStyles = makeStyles(style);
const UserProfilePicture = ({ userData }) => {
const classes = useStyles();
const { profilePictureUrl } = userData;
const url = profilePictureUrl ?? DefaultUserProfilePicture;
return <Avatar src={url} alt="..." className={classes.profilePicture} />;
};
UserProfilePicture.propTypes = {
userData: PropTypes.object.isRequired
};
export default UserProfilePicture;

View File

@ -0,0 +1,12 @@
const style = theme => {
return {
profilePicture: {
margin: "auto",
display: "block",
width: theme.spacing(25),
height: theme.spacing(25)
}
};
};
export default style;

31
src/utils/camelizeKeys.js Normal file
View File

@ -0,0 +1,31 @@
function camelizeKeys(o) {
var newO, origKey, newKey, value;
if (o instanceof Array) {
return o.map(function (value) {
if (typeof value === "object") {
value = camelizeKeys(value);
}
return value;
});
} else {
newO = {};
for (origKey in o) {
if (Object.prototype.hasOwnProperty.call(o, origKey)) {
newKey = (
origKey.charAt(0).toLowerCase() + origKey.slice(1) || origKey
).toString();
value = o[origKey];
if (
value instanceof Array ||
(value !== null && value.constructor === Object)
) {
value = camelizeKeys(value);
}
newO[newKey] = value;
}
}
}
return newO;
}
export { camelizeKeys };