Files
zipline/src/components/UI.tsx
2020-11-05 19:47:22 -08:00

439 lines
14 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import AppBar from '@material-ui/core/AppBar';
import Box from '@material-ui/core/Box';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import Drawer from '@material-ui/core/Drawer';
import Avatar from '@material-ui/core/Avatar';
import Hidden from '@material-ui/core/Hidden';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import IconButton from '@material-ui/core/IconButton';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import Snackbar from '@material-ui/core/Snackbar';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogContentText from '@material-ui/core/DialogContentText';
import DialogTitle from '@material-ui/core/DialogTitle';
import Button from '@material-ui/core/Button';
import Alert from '@material-ui/lab/Alert';
import HomeIcon from '@material-ui/icons/Home';
import DataUsageIcon from '@material-ui/icons/DataUsage';
import PhotoIcon from '@material-ui/icons/Photo';
import LinkIcon from '@material-ui/icons/Link';
import MenuIcon from '@material-ui/icons/Menu';
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
import GroupIcon from '@material-ui/icons/Group';
import FileCopyIcon from '@material-ui/icons/FileCopy';
import ExitToAppIcon from '@material-ui/icons/ExitToApp';
import PublishIcon from '@material-ui/icons/Publish';
import RotateLeftIcon from '@material-ui/icons/RotateLeft';
import Divider from '@material-ui/core/Divider';
import copy from 'copy-to-clipboard';
import { LOGOUT, UPDATE_USER } from '../reducer';
import { makeStyles, useTheme, withStyles } from '@material-ui/core/styles';
import { useRouter } from 'next/router';
import { useDispatch } from 'react-redux';
import { store } from '../store';
import { MD5 } from 'crypto-js';
const drawerWidth = 240;
const useStyles = makeStyles(theme => ({
root: {
display: 'flex',
flexGrow: 1
},
drawer: {
[theme.breakpoints.up('sm')]: {
width: drawerWidth,
flexShrink: 0
},
outlineColor: '#fff'
},
appBar: {
display: 'flex',
backgroundColor: theme.palette.type === 'dark' ? '#000' : '#fff',
color: theme.palette.type !== 'dark' ? '#000' : '#fff',
[theme.breakpoints.up('sm')]: {
width: 'calc(100%)',
marginLeft: drawerWidth
},
borderBottom: theme.palette.type === 'dark' ? '1px solid #1f1f1f' : '1px solid #e0e0e0'
},
menuButton: {
marginRight: theme.spacing(2),
[theme.breakpoints.up('sm')]: {
display: 'none'
}
},
rightButton: {
marginLeft: 'auto'
},
// necessary for content to be below app bar
toolbar: theme.mixins.toolbar,
drawerPaper: {
width: drawerWidth
},
content: {
flexGrow: 1,
padding: theme.spacing(1)
},
menuIcon: {
marginRight: '10px'
}
}));
const NoFocusMenuItem = withStyles(theme => ({
root: {
'&:hover': {
backgroundColor: theme.palette.type === 'dark' ? '#000' : '#f7f7f7'
}
}
}))(MenuItem);
export default function UI({ children }) {
const classes = useStyles();
const theme = useTheme();
const state = store.getState();
const router = useRouter();
const dispatch = useDispatch();
const [emailHash, setEmailHash] = useState('');
const [mobileOpen, setMobileOpen] = useState(false);
const [admin, setAdmin] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const [alertMessage, setAlertMessage] = useState('Copied token!');
const [tokenOpen, setTokenOpen] = useState(false);
const [resetToken, setResetToken] = useState(false);
const [alertOpen, setAlertOpen] = useState(false);
const open = Boolean(anchorEl);
useEffect(() => {
(async () => {
const d = await (await fetch('/api/user')).json();
if (d.error) {
dispatch({ type: LOGOUT });
dispatch({ type: UPDATE_USER, payload: null });
router.push('/user/login');
} else setAdmin(d.administrator);
})();
setEmailHash(MD5(state.user.email).toString());
}, []);
const handleCopyTokenThenClose = async () => {
const data = await (await fetch('/api/user')).json();
if (!data.error) {
copy(data.token);
setAlertMessage('Copied token!');
setTokenOpen(false);
setAlertOpen(true);
setAnchorEl(null);
}
};
const handleResetTokenThenClose = async () => {
const data = await (
await fetch('/api/user/reset-token', { method: 'POST' })
).json();
if (!data.error && data.updated) {
setAlertMessage('Reset token!');
setResetToken(false);
setAlertOpen(true);
}
};
const handleLogout = async () => {
const data = await (
await fetch('/api/user/logout', { method: 'POST' })
).json();
if (!data.error && data.clearStore) {
dispatch({ type: LOGOUT });
dispatch({ type: UPDATE_USER, payload: null });
setAlertMessage('Logged out!');
setAlertOpen(true);
router.push('/user/login');
}
};
const handleDrawerToggle = () => setMobileOpen(!mobileOpen);
const drawer = (
<div>
<Toolbar />
<AppBar position='fixed' className={classes.appBar} elevation={0}>
<Toolbar>
<IconButton
color='inherit'
aria-label='open drawer'
edge='start'
onClick={handleDrawerToggle}
className={classes.menuButton}
>
<MenuIcon />
</IconButton>
<Typography variant='h6'>Zipline</Typography>
<Box className={classes.rightButton}>
{state.user.email ? (
<Button
aria-label='account of current user'
aria-controls='menu-appbar'
aria-haspopup='true'
onClick={event => setAnchorEl(event.currentTarget)}
color='inherit'
className={classes.rightButton}
>
<Avatar src={`https://www.gravatar.com/avatar/${emailHash}.jpg`}>
{state.user.username[0].toUpperCase()}
</Avatar>
</Button>
) : (
<IconButton
aria-label='account of current user'
aria-controls='menu-appbar'
aria-haspopup='true'
onClick={event => setAnchorEl(event.currentTarget)}
color='inherit'
className={classes.rightButton}
>
<AccountCircleIcon className={classes.rightButton} />
</IconButton>
)}
</Box>
<Menu
id='menu-appbar'
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'top',
horizontal: 'right'
}}
keepMounted
transformOrigin={{
vertical: 'top',
horizontal: 'right'
}}
open={open}
onClose={() => setAnchorEl(null)}
>
<NoFocusMenuItem>
<Typography variant='h6'>
{state.user.username}
</Typography>
</NoFocusMenuItem>
<Divider />
<Link href='/user/manage'>
<MenuItem onClick={() => setAnchorEl(null)}>
<AccountCircleIcon className={classes.menuIcon} />
Manage Profile
</MenuItem>
</Link>
<MenuItem onClick={() => setTokenOpen(true)}>
<FileCopyIcon className={classes.menuIcon} /> Copy Token
</MenuItem>
<MenuItem onClick={() => setResetToken(true)}>
<RotateLeftIcon className={classes.menuIcon} /> Reset Token
</MenuItem>
<MenuItem onClick={handleLogout}>
<ExitToAppIcon className={classes.menuIcon} /> Logout
</MenuItem>
</Menu>
</Toolbar>
</AppBar>
<List>
<Link href='/dash'>
<ListItem button key='Home' selected={router.pathname === '/dash'}>
<ListItemIcon>
<HomeIcon />
</ListItemIcon>
<ListItemText primary='Home' />
</ListItem>
</Link>
<Link href='/dash/statistics'>
<ListItem
button
key='Statistics'
selected={router.pathname === '/dash/statistics'}
>
<ListItemIcon>
<DataUsageIcon />
</ListItemIcon>
<ListItemText primary='Statistics' />
</ListItem>
</Link>
<Link href='/dash/upload'>
<ListItem
button
key='Upload'
selected={router.pathname === '/dash/upload'}
>
<ListItemIcon>
<PublishIcon />
</ListItemIcon>
<ListItemText primary='Upload' />
</ListItem>
</Link>
<Link href='/dash/images'>
<ListItem
button
key='Images'
selected={router.pathname === '/dash/images'}
>
<ListItemIcon>
<PhotoIcon />
</ListItemIcon>
<ListItemText primary='Images' />
</ListItem>
</Link>
<Link href='/dash/urls'>
<ListItem
button
key='URLs'
selected={router.pathname === '/dash/urls'}
>
<ListItemIcon>
<LinkIcon />
</ListItemIcon>
<ListItemText primary='URLs' />
</ListItem>
</Link>
{admin ? (
<Link href='/dash/users'>
<ListItem
button
key='Users'
selected={router.pathname === '/dash/users'}
>
<ListItemIcon>
<GroupIcon />
</ListItemIcon>
<ListItemText primary='Users' />
</ListItem>
</Link>
) : null}
</List>
</div>
);
const container =
typeof window !== 'undefined' ? () => window.document.body : undefined;
return (
<div className={classes.root}>
<Snackbar
anchorOrigin={{
vertical: 'top',
horizontal: 'center'
}}
open={alertOpen}
autoHideDuration={6000}
onClose={() => setAlertOpen(false)}
>
<Alert severity='success' variant='filled'>
{alertMessage}
</Alert>
</Snackbar>
<Dialog
open={tokenOpen}
onClose={() => setTokenOpen(false)}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle id='alert-dialog-title'>Are you sure?</DialogTitle>
<DialogContent>
<DialogContentText id='alert-dialog-description'>
This token is used to upload images to Zipline, and should not be
shared!
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setTokenOpen(false)} color='primary'>
Close
</Button>
<Button onClick={handleCopyTokenThenClose} color='primary' autoFocus>
Yes, copy!
</Button>
</DialogActions>
</Dialog>
<Dialog
open={resetToken}
onClose={() => setResetToken(false)}
aria-labelledby='alert-dialog-title'
aria-describedby='alert-dialog-description'
>
<DialogTitle id='alert-dialog-title'>Are you sure?</DialogTitle>
<DialogContent>
<DialogContentText id='alert-dialog-description'>
This token is used to upload images to Zipline, resetting your token
will cause any uploading actions to not work until you update them
your self.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setResetToken(false)} color='primary'>
Close
</Button>
<Button onClick={handleResetTokenThenClose} color='primary' autoFocus>
Yes, reset!
</Button>
</DialogActions>
</Dialog>
<AppBar position='fixed' className={classes.appBar} elevation={0}>
<Toolbar>
<IconButton
color='inherit'
aria-label='open drawer'
edge='start'
onClick={handleDrawerToggle}
className={classes.menuButton}
>
<MenuIcon />
</IconButton>
<Typography variant='h6'>Zipline</Typography>
</Toolbar>
</AppBar>
<nav className={classes.drawer} aria-label='mailbox folders'>
<Hidden smUp implementation='css'>
<Drawer
container={container}
variant='temporary'
anchor={theme.direction === 'rtl' ? 'right' : 'left'}
open={mobileOpen}
onClose={handleDrawerToggle}
classes={{
paper: classes.drawerPaper
}}
ModalProps={{
keepMounted: true // Better open performance on mobile.
}}
>
{drawer}
</Drawer>
</Hidden>
<Hidden xsDown implementation='css'>
<Drawer
classes={{
paper: classes.drawerPaper
}}
variant='permanent'
open
>
{drawer}
</Drawer>
</Hidden>
</nav>
<main className={classes.content}>
<div className={classes.toolbar} />
{children}
</main>
</div>
);
}