feat: switch to mantine v5
This commit is contained in:
parent
12baadd563
commit
3ea24ddf0c
26 changed files with 1196 additions and 1149 deletions
0
.yarn/releases/yarn-3.2.1.cjs
vendored
Normal file → Executable file
0
.yarn/releases/yarn-3.2.1.cjs
vendored
Normal file → Executable file
|
@ -37,7 +37,7 @@ const { rm } = require('fs/promises');
|
||||||
write: true,
|
write: true,
|
||||||
watch,
|
watch,
|
||||||
incremental: watch,
|
incremental: watch,
|
||||||
sourcemap: false,
|
sourcemap: true,
|
||||||
minify: false,
|
minify: false,
|
||||||
});
|
});
|
||||||
})();
|
})();
|
|
@ -8,9 +8,6 @@ module.exports = {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
api: {
|
|
||||||
responseLimit: false,
|
|
||||||
},
|
|
||||||
poweredByHeader: false,
|
poweredByHeader: false,
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
};
|
};
|
28
package.json
28
package.json
|
@ -3,7 +3,7 @@
|
||||||
"version": "3.4.8",
|
"version": "3.4.8",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node esbuild.config.js && REACT_EDITOR=code NODE_ENV=development node dist/server",
|
"dev": "node esbuild.config.js && REACT_EDITOR=code NODE_ENV=development node --enable-source-maps dist/server",
|
||||||
"build": "npm-run-all build:server build:schema build:next",
|
"build": "npm-run-all build:server build:schema build:next",
|
||||||
"build:server": "node esbuild.config.js",
|
"build:server": "node esbuild.config.js",
|
||||||
"build:next": "next build",
|
"build:next": "next build",
|
||||||
|
@ -16,17 +16,21 @@
|
||||||
"docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build"
|
"docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emotion/react": "^11.9.3",
|
||||||
|
"@emotion/server": "^11.4.0",
|
||||||
"@iarna/toml": "2.2.5",
|
"@iarna/toml": "2.2.5",
|
||||||
"@mantine/core": "^4.2.9",
|
"@mantine/core": "^5.0.0",
|
||||||
"@mantine/dropzone": "^4.2.9",
|
"@mantine/dropzone": "^5.0.0",
|
||||||
"@mantine/hooks": "^4.2.9",
|
"@mantine/form": "^5.0.0",
|
||||||
"@mantine/modals": "^4.2.9",
|
"@mantine/hooks": "^5.0.0",
|
||||||
"@mantine/next": "^4.2.9",
|
"@mantine/modals": "^5.0.0",
|
||||||
"@mantine/notifications": "^4.2.9",
|
"@mantine/next": "^5.0.0",
|
||||||
"@mantine/prism": "^4.2.9",
|
"@mantine/notifications": "^5.0.0",
|
||||||
"@prisma/client": "^3.15.2",
|
"@mantine/nprogress": "^5.0.0",
|
||||||
"@prisma/migrate": "^3.15.2",
|
"@mantine/prism": "^5.0.0",
|
||||||
"@prisma/sdk": "^3.15.2",
|
"@prisma/client": "^4.1.0",
|
||||||
|
"@prisma/internals": "^4.1.0",
|
||||||
|
"@prisma/migrate": "^4.1.0",
|
||||||
"@reduxjs/toolkit": "^1.8.2",
|
"@reduxjs/toolkit": "^1.8.2",
|
||||||
"argon2": "^0.28.5",
|
"argon2": "^0.28.5",
|
||||||
"colorette": "^2.0.19",
|
"colorette": "^2.0.19",
|
||||||
|
@ -39,7 +43,7 @@
|
||||||
"minio": "^7.0.28",
|
"minio": "^7.0.28",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"next": "^12.1.6",
|
"next": "^12.1.6",
|
||||||
"prisma": "^3.15.2",
|
"prisma": "^4.1.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-feather": "^2.0.10",
|
"react-feather": "^2.0.10",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Button, Card, Grid, Group, Image as MImage, Modal, Stack, Text, Title, useMantineTheme } from '@mantine/core';
|
import { Button, Card, Group, Modal, Stack, Text, Title, useMantineTheme } from '@mantine/core';
|
||||||
import { useClipboard } from '@mantine/hooks';
|
import { useClipboard } from '@mantine/hooks';
|
||||||
import { useNotifications } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import Type from './Type';
|
import Type from './Type';
|
||||||
|
@ -21,7 +21,6 @@ export function FileMeta({ Icon, title, subtitle }) {
|
||||||
|
|
||||||
export default function File({ image, updateImages }) {
|
export default function File({ image, updateImages }) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const notif = useNotifications();
|
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
|
@ -29,14 +28,14 @@ export default function File({ image, updateImages }) {
|
||||||
const res = await useFetch('/api/user/files', 'DELETE', { id: image.id });
|
const res = await useFetch('/api/user/files', 'DELETE', { id: image.id });
|
||||||
if (!res.error) {
|
if (!res.error) {
|
||||||
updateImages(true);
|
updateImages(true);
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'File Deleted',
|
title: 'File Deleted',
|
||||||
message: '',
|
message: '',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
icon: <DeleteIcon />,
|
icon: <DeleteIcon />,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Failed to delete file',
|
title: 'Failed to delete file',
|
||||||
message: res.error,
|
message: res.error,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
|
@ -50,7 +49,7 @@ export default function File({ image, updateImages }) {
|
||||||
const handleCopy = () => {
|
const handleCopy = () => {
|
||||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${image.url}`);
|
clipboard.copy(`${window.location.protocol}//${window.location.host}${image.url}`);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Copied to clipboard',
|
title: 'Copied to clipboard',
|
||||||
message: '',
|
message: '',
|
||||||
icon: <CopyIcon />,
|
icon: <CopyIcon />,
|
||||||
|
@ -60,7 +59,7 @@ export default function File({ image, updateImages }) {
|
||||||
const handleFavorite = async () => {
|
const handleFavorite = async () => {
|
||||||
const data = await useFetch('/api/user/files', 'PATCH', { id: image.id, favorite: !image.favorite });
|
const data = await useFetch('/api/user/files', 'PATCH', { id: image.id, favorite: !image.favorite });
|
||||||
if (!data.error) updateImages(true);
|
if (!data.error) updateImages(true);
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'),
|
title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'),
|
||||||
message: '',
|
message: '',
|
||||||
icon: <StarIcon />,
|
icon: <StarIcon />,
|
||||||
|
@ -75,8 +74,6 @@ export default function File({ image, updateImages }) {
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
title={<Title>{image.file}</Title>}
|
title={<Title>{image.file}</Title>}
|
||||||
size='xl'
|
size='xl'
|
||||||
overlayBlur={3}
|
|
||||||
overlayColor={theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white'}
|
|
||||||
>
|
>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Type
|
<Type
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { AppShell, Box, Burger, Divider, Group, Header, MediaQuery, Navbar, Paper, Popover, ScrollArea, Select, Text, ThemeIcon, Title, UnstyledButton, useMantineTheme } from '@mantine/core';
|
import { AppShell, Box, Burger, Button, Divider, Header, MediaQuery, Navbar, NavLink, Paper, Popover, ScrollArea, Select, Stack, Text, Title, UnstyledButton, useMantineTheme, Group } from '@mantine/core';
|
||||||
import { useClipboard } from '@mantine/hooks';
|
import { useClipboard } from '@mantine/hooks';
|
||||||
import { useModals } from '@mantine/modals';
|
import { useModals } from '@mantine/modals';
|
||||||
import { useNotifications } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
import { updateUser } from 'lib/redux/reducers/user';
|
import { updateUser } from 'lib/redux/reducers/user';
|
||||||
import { useStoreDispatch } from 'lib/redux/store';
|
import { useStoreDispatch } from 'lib/redux/store';
|
||||||
|
@ -64,37 +64,50 @@ function MenuItem(props) {
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
icon: <HomeIcon />,
|
icon: <HomeIcon size={18} />,
|
||||||
text: 'Home',
|
text: 'Home',
|
||||||
link: '/dashboard',
|
link: '/dashboard',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <FileIcon />,
|
icon: <FileIcon size={18} />,
|
||||||
text: 'Files',
|
text: 'Files',
|
||||||
link: '/dashboard/files',
|
link: '/dashboard/files',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <ActivityIcon />,
|
icon: <ActivityIcon size={18} />,
|
||||||
text: 'Stats',
|
text: 'Stats',
|
||||||
link: '/dashboard/stats',
|
link: '/dashboard/stats',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <LinkIcon />,
|
icon: <LinkIcon size={18} />,
|
||||||
text: 'URLs',
|
text: 'URLs',
|
||||||
link: '/dashboard/urls',
|
link: '/dashboard/urls',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <UploadIcon />,
|
icon: <UploadIcon size={18} />,
|
||||||
text: 'Upload',
|
text: 'Upload',
|
||||||
link: '/dashboard/upload',
|
link: '/dashboard/upload',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <TypeIcon />,
|
icon: <TypeIcon size={18} />,
|
||||||
text: 'Upload Text',
|
text: 'Upload Text',
|
||||||
link: '/dashboard/text',
|
link: '/dashboard/text',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const admin_items = [
|
||||||
|
{
|
||||||
|
icon: <UserIcon size={18} />,
|
||||||
|
text: 'Users',
|
||||||
|
link: '/dashboard/users',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <TagIcon size={18} />,
|
||||||
|
text: 'Invites',
|
||||||
|
link: '/dashboard/invites',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export default function Layout({ children, user, title }) {
|
export default function Layout({ children, user, title }) {
|
||||||
const [token, setToken] = useState(user?.token);
|
const [token, setToken] = useState(user?.token);
|
||||||
const [systemTheme, setSystemTheme] = useState(user.systemTheme ?? 'system');
|
const [systemTheme, setSystemTheme] = useState(user.systemTheme ?? 'system');
|
||||||
|
@ -104,7 +117,6 @@ export default function Layout({ children, user, title }) {
|
||||||
const dispatch = useStoreDispatch();
|
const dispatch = useStoreDispatch();
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
const notif = useNotifications();
|
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
|
|
||||||
const handleUpdateTheme = async value => {
|
const handleUpdateTheme = async value => {
|
||||||
|
@ -116,7 +128,7 @@ export default function Layout({ children, user, title }) {
|
||||||
dispatch(updateUser(newUser));
|
dispatch(updateUser(newUser));
|
||||||
router.replace(router.pathname);
|
router.replace(router.pathname);
|
||||||
|
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: `Theme changed to ${friendlyThemeName[value]}`,
|
title: `Theme changed to ${friendlyThemeName[value]}`,
|
||||||
message: '',
|
message: '',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
|
@ -126,8 +138,6 @@ export default function Layout({ children, user, title }) {
|
||||||
|
|
||||||
const openResetToken = () => modals.openConfirmModal({
|
const openResetToken = () => modals.openConfirmModal({
|
||||||
title: 'Reset Token',
|
title: 'Reset Token',
|
||||||
centered: true,
|
|
||||||
overlayBlur: 3,
|
|
||||||
children: (
|
children: (
|
||||||
<Text size='sm'>
|
<Text size='sm'>
|
||||||
Once you reset your token, you will have to update any uploaders to use this new token.
|
Once you reset your token, you will have to update any uploaders to use this new token.
|
||||||
|
@ -138,14 +148,14 @@ export default function Layout({ children, user, title }) {
|
||||||
const a = await useFetch('/api/user/token', 'PATCH');
|
const a = await useFetch('/api/user/token', 'PATCH');
|
||||||
if (!a.success) {
|
if (!a.success) {
|
||||||
setToken(a.success);
|
setToken(a.success);
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Token Reset Failed',
|
title: 'Token Reset Failed',
|
||||||
message: a.error,
|
message: a.error,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
icon: <CrossIcon />,
|
icon: <CrossIcon />,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Token Reset',
|
title: 'Token Reset',
|
||||||
message: 'Your token has been reset. You will need to update any uploaders to use this new token.',
|
message: 'Your token has been reset. You will need to update any uploaders to use this new token.',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
|
@ -159,8 +169,6 @@ export default function Layout({ children, user, title }) {
|
||||||
|
|
||||||
const openCopyToken = () => modals.openConfirmModal({
|
const openCopyToken = () => modals.openConfirmModal({
|
||||||
title: 'Copy Token',
|
title: 'Copy Token',
|
||||||
centered: true,
|
|
||||||
overlayBlur: 3,
|
|
||||||
children: (
|
children: (
|
||||||
<Text size='sm'>
|
<Text size='sm'>
|
||||||
Make sure you don't share this token with anyone as they will be able to upload files on your behalf.
|
Make sure you don't share this token with anyone as they will be able to upload files on your behalf.
|
||||||
|
@ -170,7 +178,7 @@ export default function Layout({ children, user, title }) {
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
clipboard.copy(token);
|
clipboard.copy(token);
|
||||||
|
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Token Copied',
|
title: 'Token Copied',
|
||||||
message: 'Your token has been copied to your clipboard.',
|
message: 'Your token has been copied to your clipboard.',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
|
@ -187,7 +195,7 @@ export default function Layout({ children, user, title }) {
|
||||||
fixed
|
fixed
|
||||||
navbar={
|
navbar={
|
||||||
<Navbar
|
<Navbar
|
||||||
p='md'
|
pt='sm'
|
||||||
hiddenBreakpoint='sm'
|
hiddenBreakpoint='sm'
|
||||||
hidden={!opened}
|
hidden={!opened}
|
||||||
width={{ sm: 200, lg: 230 }}
|
width={{ sm: 200, lg: 230 }}
|
||||||
|
@ -195,84 +203,37 @@ export default function Layout({ children, user, title }) {
|
||||||
<Navbar.Section
|
<Navbar.Section
|
||||||
grow
|
grow
|
||||||
component={ScrollArea}
|
component={ScrollArea}
|
||||||
ml={-10}
|
|
||||||
mr={-10}
|
|
||||||
sx={{ paddingLeft: 10, paddingRight: 10 }}
|
|
||||||
>
|
>
|
||||||
{items.map(({ icon, text, link }) => (
|
{items.map(({ icon, text, link }) => (
|
||||||
<Link href={link} key={text} passHref>
|
<Link href={link} key={text} passHref>
|
||||||
<UnstyledButton
|
<NavLink
|
||||||
sx={{
|
component='a'
|
||||||
display: 'block',
|
label={text}
|
||||||
width: '100%',
|
icon={icon}
|
||||||
padding: theme.spacing.xs,
|
active={router.pathname === link}
|
||||||
borderRadius: theme.radius.sm,
|
variant='light'
|
||||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.black,
|
/>
|
||||||
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: theme.other.hover,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Group>
|
|
||||||
<ThemeIcon color='primary' variant='filled'>
|
|
||||||
{icon}
|
|
||||||
</ThemeIcon>
|
|
||||||
|
|
||||||
<Text size='lg'>{text}</Text>
|
|
||||||
</Group>
|
|
||||||
</UnstyledButton>
|
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
{user.administrator && (
|
{user.administrator && (
|
||||||
<>
|
<NavLink
|
||||||
<Link href='/dashboard/users' passHref>
|
label='Administration'
|
||||||
<UnstyledButton
|
icon={<SettingsIcon />}
|
||||||
sx={{
|
childrenOffset={28}
|
||||||
display: 'block',
|
defaultOpened={admin_items.map(x => x.link).includes(router.pathname)}
|
||||||
width: '100%',
|
>
|
||||||
padding: theme.spacing.xs,
|
{admin_items.map(({ icon, text, link }) => (
|
||||||
borderRadius: theme.radius.sm,
|
<Link href={link} key={text} passHref>
|
||||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.black,
|
<NavLink
|
||||||
|
component='a'
|
||||||
'&:hover': {
|
label={text}
|
||||||
backgroundColor: theme.other.hover,
|
icon={icon}
|
||||||
},
|
active={router.pathname === link}
|
||||||
}}
|
variant='light'
|
||||||
>
|
/>
|
||||||
<Group>
|
</Link>
|
||||||
<ThemeIcon color='primary' variant='filled'>
|
))}
|
||||||
<UserIcon />
|
</NavLink>
|
||||||
</ThemeIcon>
|
|
||||||
|
|
||||||
<Text size='lg'>Users</Text>
|
|
||||||
</Group>
|
|
||||||
</UnstyledButton>
|
|
||||||
</Link>
|
|
||||||
<Link href='/dashboard/invites' passHref>
|
|
||||||
<UnstyledButton
|
|
||||||
sx={{
|
|
||||||
display: 'block',
|
|
||||||
width: '100%',
|
|
||||||
padding: theme.spacing.xs,
|
|
||||||
borderRadius: theme.radius.sm,
|
|
||||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[0] : theme.black,
|
|
||||||
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: theme.other.hover,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Group>
|
|
||||||
<ThemeIcon color='primary' variant='filled'>
|
|
||||||
<TagIcon />
|
|
||||||
</ThemeIcon>
|
|
||||||
|
|
||||||
<Text size='lg'>Invites</Text>
|
|
||||||
</Group>
|
|
||||||
</UnstyledButton>
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</Navbar.Section>
|
</Navbar.Section>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
|
@ -291,75 +252,78 @@ export default function Layout({ children, user, title }) {
|
||||||
<Title ml='md'>{title}</Title>
|
<Title ml='md'>{title}</Title>
|
||||||
<Box sx={{ marginLeft: 'auto', marginRight: 0 }}>
|
<Box sx={{ marginLeft: 'auto', marginRight: 0 }}>
|
||||||
<Popover
|
<Popover
|
||||||
position='top'
|
position='bottom-end'
|
||||||
placement='end'
|
|
||||||
spacing={4}
|
|
||||||
opened={open}
|
opened={open}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
target={
|
|
||||||
<UnstyledButton
|
|
||||||
onClick={() => setOpen(!open)}
|
|
||||||
sx={{
|
|
||||||
display: 'block',
|
|
||||||
width: '100%',
|
|
||||||
padding: theme.spacing.xs,
|
|
||||||
borderRadius: theme.radius.sm,
|
|
||||||
color: theme.other.color,
|
|
||||||
|
|
||||||
'&:hover': {
|
|
||||||
backgroundColor: theme.other.hover,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Group>
|
|
||||||
<ThemeIcon color='primary' variant='filled'>
|
|
||||||
<SettingsIcon />
|
|
||||||
</ThemeIcon>
|
|
||||||
<Text>{user.username}</Text>
|
|
||||||
</Group>
|
|
||||||
</UnstyledButton>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Group direction='column' spacing={2}>
|
<Popover.Target>
|
||||||
<Text sx={{
|
<Button
|
||||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
|
leftIcon={<SettingsIcon />}
|
||||||
fontWeight: 500,
|
onClick={() => setOpen((o) => !o)}
|
||||||
fontSize: theme.fontSizes.sm,
|
sx={t => ({
|
||||||
padding: `${theme.spacing.xs / 2}px ${theme.spacing.sm}px`,
|
backgroundColor: '#00000000',
|
||||||
cursor: 'default',
|
'&:hover': {
|
||||||
}}
|
backgroundColor: t.other.hover,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
size='xl'
|
||||||
|
p='sm'
|
||||||
>
|
>
|
||||||
{user.username}
|
{user.username}
|
||||||
</Text>
|
</Button>
|
||||||
<MenuItemLink icon={<SettingsIcon />} href='/dashboard/manage'>Manage Account</MenuItemLink>
|
</Popover.Target>
|
||||||
<MenuItem icon={<CopyIcon />} onClick={() => {setOpen(false);openCopyToken();}}>Copy Token</MenuItem>
|
|
||||||
<MenuItem icon={<DeleteIcon />} onClick={() => {setOpen(false);openResetToken();}} color='red'>Reset Token</MenuItem>
|
<Popover.Dropdown p={4}>
|
||||||
<MenuItemLink icon={<LogoutIcon />} href='/auth/logout' color='red'>Logout</MenuItemLink>
|
<Stack spacing={2}>
|
||||||
<Divider
|
<Text sx={{
|
||||||
variant='solid'
|
color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
|
||||||
my={theme.spacing.xs / 2}
|
fontWeight: 500,
|
||||||
sx={theme => ({
|
fontSize: theme.fontSizes.sm,
|
||||||
width: '110%',
|
padding: `${theme.spacing.xs / 2}px ${theme.spacing.sm}px`,
|
||||||
borderTopColor: theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.colors.gray[2],
|
cursor: 'default',
|
||||||
margin: `${theme.spacing.xs / 2}px -4px`,
|
}}
|
||||||
})}
|
>
|
||||||
/>
|
{user.username}
|
||||||
<MenuItem icon={<PencilIcon />}>
|
</Text>
|
||||||
<Select
|
<MenuItemLink icon={<SettingsIcon />} href='/dashboard/manage'>Manage Account</MenuItemLink>
|
||||||
size='xs'
|
<MenuItem icon={<CopyIcon />} onClick={() => {setOpen(false);openCopyToken();}}>Copy Token</MenuItem>
|
||||||
data={Object.keys(themes).map(t => ({ value: t, label: friendlyThemeName[t] }))}
|
<MenuItem icon={<DeleteIcon />} onClick={() => {setOpen(false);openResetToken();}} color='red'>Reset Token</MenuItem>
|
||||||
value={systemTheme}
|
<MenuItemLink icon={<LogoutIcon />} href='/auth/logout' color='red'>Logout</MenuItemLink>
|
||||||
onChange={handleUpdateTheme}
|
<Divider
|
||||||
|
variant='solid'
|
||||||
|
my={theme.spacing.xs / 2}
|
||||||
|
sx={theme => ({
|
||||||
|
width: '110%',
|
||||||
|
borderTopColor: theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.colors.gray[2],
|
||||||
|
margin: `${theme.spacing.xs / 2}px -4px`,
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
</MenuItem>
|
<MenuItem icon={<PencilIcon />}>
|
||||||
</Group>
|
<Select
|
||||||
|
size='xs'
|
||||||
|
data={Object.keys(themes).map(t => ({ value: t, label: friendlyThemeName[t] }))}
|
||||||
|
value={systemTheme}
|
||||||
|
onChange={handleUpdateTheme}
|
||||||
|
/>
|
||||||
|
</MenuItem>
|
||||||
|
</Stack>
|
||||||
|
</Popover.Dropdown>
|
||||||
</Popover>
|
</Popover>
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Paper withBorder p='md' shadow='xs'>{children}</Paper>
|
<Paper
|
||||||
|
withBorder
|
||||||
|
p='md'
|
||||||
|
shadow='xs'
|
||||||
|
sx={t => ({
|
||||||
|
borderColor: t.colorScheme === 'dark' ? t.colors.dark[5] : t.colors.dark[0],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Paper>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import { Text } from '@mantine/core';
|
import { Text } from '@mantine/core';
|
||||||
|
|
||||||
export default function MutedText({ children, ...props }) {
|
export default function MutedText({ children, ...props }) {
|
||||||
return <Text color='gray' size='xl' {...props}>{children}</Text>;
|
return <Text color='dimmed' size='xl' {...props}>{children}</Text>;
|
||||||
}
|
}
|
|
@ -49,27 +49,30 @@ export default function PasswordStrength({ value, setValue, setStrength, ...prop
|
||||||
return (
|
return (
|
||||||
<Popover
|
<Popover
|
||||||
opened={popoverOpened}
|
opened={popoverOpened}
|
||||||
position='bottom'
|
position='top'
|
||||||
placement='start'
|
width='target'
|
||||||
withArrow
|
withArrow
|
||||||
trapFocus={false}
|
trapFocus={false}
|
||||||
transition='pop-top-left'
|
|
||||||
onFocusCapture={() => setPopoverOpened(true)}
|
|
||||||
onBlurCapture={() => setPopoverOpened(false)}
|
|
||||||
styles={{ root: { width: '100%' } }}
|
|
||||||
target={
|
|
||||||
<PasswordInput
|
|
||||||
label='Password'
|
|
||||||
description='Strong password should include letters in lower and uppercase, at least 1 number, at least 1 special symbol'
|
|
||||||
value={value}
|
|
||||||
onChange={(event) => setValue(event.currentTarget.value)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Progress color={color} value={strength} size={7} mb='md' />
|
<Popover.Target>
|
||||||
<PasswordRequirement label='Includes at least 8 characters' meets={value.length > 7} />
|
<div
|
||||||
{checks}
|
onFocusCapture={() => setPopoverOpened(true)}
|
||||||
|
onBlurCapture={() => setPopoverOpened(false)}
|
||||||
|
>
|
||||||
|
<PasswordInput
|
||||||
|
label='Password'
|
||||||
|
description='A strong password should include letters in lower and uppercase, at least 1 number, at least 1 special symbol'
|
||||||
|
value={value}
|
||||||
|
onChange={(event) => setValue(event.currentTarget.value)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown sx={{ }}>
|
||||||
|
<Progress color={color} value={strength} size={7} mb='md' />
|
||||||
|
<PasswordRequirement label='Includes at least 8 characters' meets={value.length > 7} />
|
||||||
|
{checks}
|
||||||
|
</Popover.Dropdown>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,24 +64,34 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
||||||
<MantineProvider
|
<MantineProvider
|
||||||
withGlobalStyles
|
withGlobalStyles
|
||||||
withNormalizeCSS
|
withNormalizeCSS
|
||||||
theme={theme}
|
theme={{
|
||||||
styles={{
|
...theme,
|
||||||
AppShell: t => ({
|
components: {
|
||||||
root: {
|
AppShell: {
|
||||||
backgroundColor: t.other.AppShell_backgroundColor,
|
styles: t => ({
|
||||||
|
root: {
|
||||||
|
backgroundColor: t.other.AppShell_backgroundColor,
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
}),
|
NavLink: {
|
||||||
Popover: {
|
styles: t => ({
|
||||||
inner: {
|
icon: {
|
||||||
width: 200,
|
paddingLeft: t.spacing.sm,
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
},
|
Modal: {
|
||||||
Accordion: {
|
defaultProps: {
|
||||||
itemTitle: {
|
centered: true,
|
||||||
border: 0,
|
overlayBlur: 3,
|
||||||
|
overlayColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
itemOpened: {
|
Popover: {
|
||||||
border: 0,
|
defaultProps: {
|
||||||
|
transition: 'pop',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import { Group, Image, Stack, Text } from '@mantine/core';
|
import { Group, Image, Text } from '@mantine/core';
|
||||||
import { Prism } from '@mantine/prism';
|
import { Prism } from '@mantine/prism';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { AudioIcon, FileIcon, PlayIcon, TypeIcon, VideoIcon } from './icons';
|
import { AudioIcon, FileIcon, PlayIcon } from './icons';
|
||||||
import MutedText from './MutedText';
|
|
||||||
|
|
||||||
function Placeholder({ text, Icon, ...props }) {
|
function Placeholder({ text, Icon, ...props }) {
|
||||||
return (
|
return (
|
||||||
<Image height={200} withPlaceholder placeholder={
|
<Image height={200} withPlaceholder placeholder={
|
||||||
<Group>
|
<Group>
|
||||||
<Icon size={48} />
|
<Icon size={48} />
|
||||||
<Text>{text}</Text>
|
<Text size='md'>{text}</Text>
|
||||||
</Group>
|
</Group>
|
||||||
} {...props} />
|
} {...props} />
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,52 +1,24 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Dropzone as MantineDropzone } from '@mantine/dropzone';
|
import { Dropzone as MantineDropzone } from '@mantine/dropzone';
|
||||||
import { Group, Text, useMantineTheme } from '@mantine/core';
|
import { Group, Text, useMantineTheme } from '@mantine/core';
|
||||||
import { CrossIcon, UploadIcon, ImageIcon } from 'components/icons';
|
import { ImageIcon } from 'components/icons';
|
||||||
|
|
||||||
function ImageUploadIcon({ status, ...props }) {
|
|
||||||
if (status.accepted) {
|
|
||||||
return <UploadIcon {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status.rejected) {
|
|
||||||
return <CrossIcon {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <ImageIcon {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIconColor(status, theme) {
|
|
||||||
return status.accepted
|
|
||||||
? theme.colors[theme.primaryColor][6]
|
|
||||||
: status.rejected
|
|
||||||
? theme.colors.red[6]
|
|
||||||
: theme.colorScheme === 'dark'
|
|
||||||
? theme.colors.dark[0]
|
|
||||||
: theme.black;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default function Dropzone({ loading, onDrop, children }) {
|
export default function Dropzone({ loading, onDrop, children }) {
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MantineDropzone loading={loading} onDrop={onDrop}>
|
<MantineDropzone onDrop={onDrop}>
|
||||||
{status => (
|
<Group position='center' spacing='xl' style={{ minHeight: 440 }}>
|
||||||
<>
|
<ImageIcon size={80} />
|
||||||
<Group position='center' spacing='xl' style={{ minHeight: 440, pointerEvents: 'none' }}>
|
|
||||||
<ImageUploadIcon
|
|
||||||
status={status}
|
|
||||||
style={{ width: 80, height: 80, color: getIconColor(status, theme) }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Text size='xl' inline>
|
<Text size='xl' inline>
|
||||||
Drag images here or click to select files
|
Drag images here or click to select files
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{children}
|
<div style={{ pointerEvents: 'all' }}>
|
||||||
</>
|
{children}
|
||||||
)}
|
</div>
|
||||||
</MantineDropzone>
|
</MantineDropzone>
|
||||||
);
|
);
|
||||||
}
|
}
|
|
@ -11,6 +11,7 @@ export function FilePreview({ file }: { file: File }) {
|
||||||
style={{ maxWidth: '10vw', maxHeight: '100vh' }}
|
style={{ maxWidth: '10vw', maxHeight: '100vh' }}
|
||||||
src={URL.createObjectURL(file)}
|
src={URL.createObjectURL(file)}
|
||||||
alt={file.name}
|
alt={file.name}
|
||||||
|
popup
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -21,13 +22,11 @@ export default function FileDropzone({ file }: { file: File }) {
|
||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
position='top'
|
position='top'
|
||||||
placement='center'
|
|
||||||
allowPointerEvents
|
|
||||||
label={
|
label={
|
||||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<FilePreview file={file} />
|
<FilePreview file={file} />
|
||||||
|
|
||||||
<Table sx={{ color: theme.colorScheme === 'dark' ? 'black' : 'white' }} ml='md'>
|
<Table sx={{ color: theme.colorScheme === 'dark' ? 'white' : 'white' }} ml='md'>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Name</td>
|
<td>Name</td>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { SimpleGrid, Skeleton, Text, Title } from '@mantine/core';
|
import { SimpleGrid, Skeleton, Text, Title } from '@mantine/core';
|
||||||
import { randomId, useClipboard } from '@mantine/hooks';
|
import { randomId, useClipboard } from '@mantine/hooks';
|
||||||
import { useNotifications } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
import Card from 'components/Card';
|
import Card from 'components/Card';
|
||||||
import File from 'components/File';
|
import File from 'components/File';
|
||||||
import { CopyIcon, CrossIcon, DeleteIcon } from 'components/icons';
|
import { CopyIcon, CrossIcon, DeleteIcon } from 'components/icons';
|
||||||
|
@ -21,7 +21,6 @@ export default function Dashboard() {
|
||||||
const [recent, setRecent] = useState([]);
|
const [recent, setRecent] = useState([]);
|
||||||
const [stats, setStats] = useState(null);
|
const [stats, setStats] = useState(null);
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
const notif = useNotifications();
|
|
||||||
|
|
||||||
const updateImages = async () => {
|
const updateImages = async () => {
|
||||||
const imgs = await useFetch('/api/user/files');
|
const imgs = await useFetch('/api/user/files');
|
||||||
|
@ -36,26 +35,26 @@ export default function Dashboard() {
|
||||||
const res = await useFetch('/api/user/files', 'DELETE', { id: original.id });
|
const res = await useFetch('/api/user/files', 'DELETE', { id: original.id });
|
||||||
if (!res.error) {
|
if (!res.error) {
|
||||||
updateImages();
|
updateImages();
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Image Deleted',
|
title: 'Image Deleted',
|
||||||
message: '',
|
message: '',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
icon: <DeleteIcon />,
|
icon: <DeleteIcon />,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Failed to delete image',
|
title: 'Failed to delete image',
|
||||||
message: res.error,
|
message: res.error,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
icon: <CrossIcon />,
|
icon: <CrossIcon />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyImage = async ({ original }) => {
|
const copyImage = async ({ original }) => {
|
||||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${original.url}`);
|
clipboard.copy(`${window.location.protocol}//${window.location.host}${original.url}`);
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Copied to clipboard',
|
title: 'Copied to clipboard',
|
||||||
message: '',
|
message: '',
|
||||||
icon: <CopyIcon />,
|
icon: <CopyIcon />,
|
||||||
|
@ -69,11 +68,11 @@ export default function Dashboard() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateImages();
|
updateImages();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>Welcome back, {user?.username}</Title>
|
<Title>Welcome back, {user?.username}</Title>
|
||||||
<Text color='gray' mb='sm'>You have <b>{images.length ? images.length : '...'}</b> files</Text>
|
<MutedText size='md'>You have <b>{images.length ? images.length : '...'}</b> files</MutedText>
|
||||||
|
|
||||||
<Title>Recent Files</Title>
|
<Title>Recent Files</Title>
|
||||||
<SimpleGrid
|
<SimpleGrid
|
||||||
|
@ -85,15 +84,15 @@ export default function Dashboard() {
|
||||||
>
|
>
|
||||||
{recent.length ? recent.map(image => (
|
{recent.length ? recent.map(image => (
|
||||||
<File key={randomId()} image={image} updateImages={updateImages} />
|
<File key={randomId()} image={image} updateImages={updateImages} />
|
||||||
)) : [1,2,3,4].map(x => (
|
)) : [1, 2, 3, 4].map(x => (
|
||||||
<div key={x}>
|
<div key={x}>
|
||||||
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }}/>
|
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
<Title mt='md'>Stats</Title>
|
<Title mt='md'>Stats</Title>
|
||||||
<Text>View more stats here <Link href='/dashboard/stats'>here</Link>.</Text>
|
<MutedText size='md'>View more stats here <Link href='/dashboard/stats'>here</Link>.</MutedText>
|
||||||
<SimpleGrid
|
<SimpleGrid
|
||||||
cols={3}
|
cols={3}
|
||||||
spacing='lg'
|
spacing='lg'
|
||||||
|
@ -117,7 +116,7 @@ export default function Dashboard() {
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
<Title mt='md'>Files</Title>
|
<Title mt='md'>Files</Title>
|
||||||
<Text>View your gallery <Link href='/dashboard/files'>here</Link>.</Text>
|
<MutedText size='md'>View your gallery <Link href='/dashboard/files'>here</Link>.</MutedText>
|
||||||
<ImagesTable
|
<ImagesTable
|
||||||
columns={[
|
columns={[
|
||||||
{ accessor: 'file', Header: 'Name', minWidth: 170, align: 'inherit' as Aligns },
|
{ accessor: 'file', Header: 'Name', minWidth: 170, align: 'inherit' as Aligns },
|
||||||
|
|
|
@ -34,39 +34,37 @@ export default function Files() {
|
||||||
</Group>
|
</Group>
|
||||||
{favoritePages.length ? (
|
{favoritePages.length ? (
|
||||||
<Accordion
|
<Accordion
|
||||||
offsetIcon={false}
|
variant='contained'
|
||||||
sx={t => ({
|
mb='sm'
|
||||||
marginTop: 2,
|
|
||||||
border: '1px solid',
|
|
||||||
marginBottom: 12,
|
|
||||||
borderColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0] ,
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
<Accordion.Item label={<Title>Favorite Files</Title>}>
|
<Accordion.Item value='favorite'>
|
||||||
<SimpleGrid
|
<Accordion.Control>Favorite Files</Accordion.Control>
|
||||||
cols={3}
|
<Accordion.Panel>
|
||||||
spacing='lg'
|
<SimpleGrid
|
||||||
breakpoints={[
|
cols={3}
|
||||||
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
|
spacing='lg'
|
||||||
]}
|
breakpoints={[
|
||||||
>
|
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
|
||||||
{favoritePages.length ? favoritePages[(favoritePage - 1) ?? 0].map(image => (
|
]}
|
||||||
<div key={image.id}>
|
>
|
||||||
<File image={image} updateImages={() => updatePages(true)} />
|
{favoritePages.length ? favoritePages[(favoritePage - 1) ?? 0].map(image => (
|
||||||
</div>
|
<div key={image.id}>
|
||||||
)) : null}
|
<File image={image} updateImages={() => updatePages(true)} />
|
||||||
</SimpleGrid>
|
</div>
|
||||||
<Box
|
)) : null}
|
||||||
sx={{
|
</SimpleGrid>
|
||||||
display: 'flex',
|
<Box
|
||||||
justifyContent: 'center',
|
sx={{
|
||||||
alignItems: 'center',
|
display: 'flex',
|
||||||
paddingTop: 12,
|
justifyContent: 'center',
|
||||||
paddingBottom: 3,
|
alignItems: 'center',
|
||||||
}}
|
paddingTop: 12,
|
||||||
>
|
paddingBottom: 3,
|
||||||
<Pagination total={favoritePages.length} page={favoritePage} onChange={setFavoritePage}/>
|
}}
|
||||||
</Box>
|
>
|
||||||
|
<Pagination total={favoritePages.length} page={favoritePage} onChange={setFavoritePage}/>
|
||||||
|
</Box>
|
||||||
|
</Accordion.Panel>
|
||||||
</Accordion.Item>
|
</Accordion.Item>
|
||||||
</Accordion>
|
</Accordion>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { ActionIcon, Avatar, Button, Card, Group, Modal, Select, SimpleGrid, Skeleton, Stack, Switch, TextInput, Title } from '@mantine/core';
|
import { ActionIcon, Avatar, Button, Card, Group, Modal, Select, SimpleGrid, Skeleton, Stack, Switch, TextInput, Title } from '@mantine/core';
|
||||||
import { useClipboard, useForm } from '@mantine/hooks';
|
import { useClipboard } from '@mantine/hooks';
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
import { useModals } from '@mantine/modals';
|
import { useModals } from '@mantine/modals';
|
||||||
import { useNotifications } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
import { CopyIcon, CrossIcon, DeleteIcon, PlusIcon, TagIcon } from 'components/icons';
|
import { CopyIcon, CrossIcon, DeleteIcon, PlusIcon, TagIcon } from 'components/icons';
|
||||||
import MutedText from 'components/MutedText';
|
import MutedText from 'components/MutedText';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
|
@ -26,7 +27,6 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
|
||||||
expires: '30m',
|
expires: '30m',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const notif = useNotifications();
|
|
||||||
|
|
||||||
const onSubmit = async values => {
|
const onSubmit = async values => {
|
||||||
if (!expires.includes(values.expires)) return form.setFieldError('expires', 'Invalid expiration');
|
if (!expires.includes(values.expires)) return form.setFieldError('expires', 'Invalid expiration');
|
||||||
|
@ -47,14 +47,14 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Failed to create invite',
|
title: 'Failed to create invite',
|
||||||
message: res.error,
|
message: res.error,
|
||||||
icon: <CrossIcon />,
|
icon: <CrossIcon />,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Created invite',
|
title: 'Created invite',
|
||||||
message: '',
|
message: '',
|
||||||
icon: <TagIcon />,
|
icon: <TagIcon />,
|
||||||
|
@ -70,9 +70,6 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
|
||||||
opened={open}
|
opened={open}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
title={<Title>Create Invite</Title>}
|
title={<Title>Create Invite</Title>}
|
||||||
overlayBlur={3}
|
|
||||||
centered={true}
|
|
||||||
|
|
||||||
>
|
>
|
||||||
<form onSubmit={form.onSubmit(v => onSubmit(v))}>
|
<form onSubmit={form.onSubmit(v => onSubmit(v))}>
|
||||||
<Select
|
<Select
|
||||||
|
@ -103,7 +100,6 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
|
||||||
|
|
||||||
export default function Users() {
|
export default function Users() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const notif = useNotifications();
|
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
|
|
||||||
|
@ -118,14 +114,14 @@ export default function Users() {
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
const res = await useFetch(`/api/auth/invite?code=${invite.code}`, 'DELETE');
|
const res = await useFetch(`/api/auth/invite?code=${invite.code}`, 'DELETE');
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Failed to delete invite ${invite.code}',
|
title: 'Failed to delete invite ${invite.code}',
|
||||||
message: res.error,
|
message: res.error,
|
||||||
icon: <CrossIcon />,
|
icon: <CrossIcon />,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: `Deleted invite ${invite.code}`,
|
title: `Deleted invite ${invite.code}`,
|
||||||
message: '',
|
message: '',
|
||||||
icon: <DeleteIcon />,
|
icon: <DeleteIcon />,
|
||||||
|
@ -139,7 +135,7 @@ export default function Users() {
|
||||||
|
|
||||||
const handleCopy = async invite => {
|
const handleCopy = async invite => {
|
||||||
clipboard.copy(`${window.location.protocol}//${window.location.host}/invite/${invite.code}`);
|
clipboard.copy(`${window.location.protocol}//${window.location.host}/invite/${invite.code}`);
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Copied to clipboard',
|
title: 'Copied to clipboard',
|
||||||
message: '',
|
message: '',
|
||||||
icon: <CopyIcon />,
|
icon: <CopyIcon />,
|
||||||
|
@ -156,7 +152,6 @@ export default function Users() {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(invites);
|
|
||||||
updateInvites();
|
updateInvites();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { Box, Button, Card, ColorInput, Group, MultiSelect, Space, Text, TextInput, PasswordInput, Title, Tooltip } from '@mantine/core';
|
import { Box, Button, Card, ColorInput, Group, MultiSelect, Space, Text, TextInput, PasswordInput, Title, Tooltip } from '@mantine/core';
|
||||||
import { randomId, useForm, useInterval } from '@mantine/hooks';
|
import { randomId, useInterval } from '@mantine/hooks';
|
||||||
|
import { useForm } from '@mantine/form';
|
||||||
import { useModals } from '@mantine/modals';
|
import { useModals } from '@mantine/modals';
|
||||||
import { useNotifications } from '@mantine/notifications';
|
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||||
import { CrossIcon, DeleteIcon } from 'components/icons';
|
import { CrossIcon, DeleteIcon } from 'components/icons';
|
||||||
import DownloadIcon from 'components/icons/DownloadIcon';
|
import DownloadIcon from 'components/icons/DownloadIcon';
|
||||||
import Link from 'components/Link';
|
import Link from 'components/Link';
|
||||||
|
@ -11,31 +12,15 @@ import { bytesToRead } from 'lib/clientUtils';
|
||||||
import { updateUser } from 'lib/redux/reducers/user';
|
import { updateUser } from 'lib/redux/reducers/user';
|
||||||
import { useStoreDispatch, useStoreSelector } from 'lib/redux/store';
|
import { useStoreDispatch, useStoreSelector } from 'lib/redux/store';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import MutedText from 'components/MutedText';
|
||||||
function VarsTooltip({ children }) {
|
|
||||||
return (
|
|
||||||
<Tooltip position='top' placement='center' color='' label={
|
|
||||||
<>
|
|
||||||
<Text><b>{'{image.file}'}</b> - file name</Text>
|
|
||||||
<Text><b>{'{image.mimetype}'}</b> - mimetype</Text>
|
|
||||||
<Text><b>{'{image.id}'}</b> - id of the image</Text>
|
|
||||||
<Text><b>{'{user.name}'}</b> - your username</Text>
|
|
||||||
visit <Link href='https://zipl.vercel.app/docs/variables'>the docs</Link> for more variables
|
|
||||||
</>
|
|
||||||
}>
|
|
||||||
{children}
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ExportDataTooltip({ children }) {
|
function ExportDataTooltip({ children }) {
|
||||||
return <Tooltip position='top' placement='center' color='' label='After clicking, if you have a lot of files the export can take a while to complete. A list of previous exports will be below to download.'>{children}</Tooltip>;
|
return <Tooltip position='top' color='' label='After clicking, if you have a lot of files the export can take a while to complete. A list of previous exports will be below to download.'>{children}</Tooltip>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Manage() {
|
export default function Manage() {
|
||||||
const user = useStoreSelector(state => state.user);
|
const user = useStoreSelector(state => state.user);
|
||||||
const dispatch = useStoreDispatch();
|
const dispatch = useStoreDispatch();
|
||||||
const notif = useNotifications();
|
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
|
||||||
const [exports, setExports] = useState([]);
|
const [exports, setExports] = useState([]);
|
||||||
|
@ -87,7 +72,8 @@ export default function Manage() {
|
||||||
|
|
||||||
if (cleanUsername === '') return form.setFieldError('username', 'Username can\'t be nothing');
|
if (cleanUsername === '') return form.setFieldError('username', 'Username can\'t be nothing');
|
||||||
|
|
||||||
const id = notif.showNotification({
|
showNotification({
|
||||||
|
id: 'update-user',
|
||||||
title: 'Updating user...',
|
title: 'Updating user...',
|
||||||
message: '',
|
message: '',
|
||||||
loading: true,
|
loading: true,
|
||||||
|
@ -107,7 +93,8 @@ export default function Manage() {
|
||||||
|
|
||||||
if (newUser.error) {
|
if (newUser.error) {
|
||||||
if (newUser.invalidDomains) {
|
if (newUser.invalidDomains) {
|
||||||
notif.updateNotification(id, {
|
updateNotification({
|
||||||
|
id: 'update-user',
|
||||||
message: <>
|
message: <>
|
||||||
<Text mt='xs'>The following domains are invalid:</Text>
|
<Text mt='xs'>The following domains are invalid:</Text>
|
||||||
{newUser.invalidDomains.map(err => (
|
{newUser.invalidDomains.map(err => (
|
||||||
|
@ -121,7 +108,8 @@ export default function Manage() {
|
||||||
icon: <CrossIcon />,
|
icon: <CrossIcon />,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
notif.updateNotification(id, {
|
updateNotification({
|
||||||
|
id: 'update-user',
|
||||||
title: 'Couldn\'t save user',
|
title: 'Couldn\'t save user',
|
||||||
message: newUser.error,
|
message: newUser.error,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
|
@ -129,7 +117,8 @@ export default function Manage() {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
dispatch(updateUser(newUser));
|
dispatch(updateUser(newUser));
|
||||||
notif.updateNotification(id, {
|
updateNotification({
|
||||||
|
id: 'update-user',
|
||||||
title: 'Saved User',
|
title: 'Saved User',
|
||||||
message: '',
|
message: '',
|
||||||
});
|
});
|
||||||
|
@ -139,7 +128,7 @@ export default function Manage() {
|
||||||
const exportData = async () => {
|
const exportData = async () => {
|
||||||
const res = await useFetch('/api/user/export', 'POST');
|
const res = await useFetch('/api/user/export', 'POST');
|
||||||
if (res.url) {
|
if (res.url) {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Export started...',
|
title: 'Export started...',
|
||||||
loading: true,
|
loading: true,
|
||||||
message: 'If you have a lot of files, the export may take a while. The list of exports will be updated every 30s.',
|
message: 'If you have a lot of files, the export may take a while. The list of exports will be updated every 30s.',
|
||||||
|
@ -163,14 +152,14 @@ export default function Manage() {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.count) {
|
if (!res.count) {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Couldn\'t delete files',
|
title: 'Couldn\'t delete files',
|
||||||
message: res.error,
|
message: res.error,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
icon: <CrossIcon />,
|
icon: <CrossIcon />,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Deleted files',
|
title: 'Deleted files',
|
||||||
message: `${res.count} files deleted`,
|
message: `${res.count} files deleted`,
|
||||||
color: 'green',
|
color: 'green',
|
||||||
|
@ -182,14 +171,10 @@ export default function Manage() {
|
||||||
const openDeleteModal = () => modals.openConfirmModal({
|
const openDeleteModal = () => modals.openConfirmModal({
|
||||||
title: 'Are you sure you want to delete all of your images?',
|
title: 'Are you sure you want to delete all of your images?',
|
||||||
closeOnConfirm: false,
|
closeOnConfirm: false,
|
||||||
centered: true,
|
|
||||||
overlayBlur: 3,
|
|
||||||
labels: { confirm: 'Yes', cancel: 'No' },
|
labels: { confirm: 'Yes', cancel: 'No' },
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
title: 'Are you really sure?',
|
title: 'Are you really sure?',
|
||||||
centered: true,
|
|
||||||
overlayBlur: 3,
|
|
||||||
labels: { confirm: 'Yes', cancel: 'No' },
|
labels: { confirm: 'Yes', cancel: 'No' },
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
handleDelete();
|
handleDelete();
|
||||||
|
@ -211,9 +196,7 @@ export default function Manage() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title>Manage User</Title>
|
<Title>Manage User</Title>
|
||||||
<VarsTooltip>
|
<MutedText size='md'>Want to use variables in embed text? Visit <Link href='https://zipline.diced.cf/docs/variables'>the docs</Link> for variables</MutedText>
|
||||||
<Text color='gray'>Want to use variables in embed text? Hover on this or visit <Link href='https://zipline.diced.cf/docs/variables'>the docs</Link> for more variables</Text>
|
|
||||||
</VarsTooltip>
|
|
||||||
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
|
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
|
||||||
<TextInput id='username' label='Username' {...form.getInputProps('username')} />
|
<TextInput id='username' label='Username' {...form.getInputProps('username')} />
|
||||||
<PasswordInput id='password' label='Password' description='Leave blank to keep your old password' {...form.getInputProps('password')} />
|
<PasswordInput id='password' label='Password' description='Leave blank to keep your old password' {...form.getInputProps('password')} />
|
||||||
|
@ -242,7 +225,7 @@ export default function Manage() {
|
||||||
|
|
||||||
<Box mb='md'>
|
<Box mb='md'>
|
||||||
<Title>Manage Data</Title>
|
<Title>Manage Data</Title>
|
||||||
<Text color='gray'>Delete, or export your data into a zip file.</Text>
|
<MutedText size='md'>Delete, or export your data into a zip file.</MutedText>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group>
|
<Group>
|
||||||
|
@ -250,17 +233,21 @@ export default function Manage() {
|
||||||
<ExportDataTooltip><Button onClick={exportData} rightIcon={<DownloadIcon />}>Export Data</Button></ExportDataTooltip>
|
<ExportDataTooltip><Button onClick={exportData} rightIcon={<DownloadIcon />}>Export Data</Button></ExportDataTooltip>
|
||||||
</Group>
|
</Group>
|
||||||
<Card mt={22}>
|
<Card mt={22}>
|
||||||
<SmallTable
|
{exports && exports.length ? (
|
||||||
columns={[
|
<SmallTable
|
||||||
{ id: 'name', name: 'Name' },
|
columns={[
|
||||||
{ id: 'date', name: 'Date' },
|
{ id: 'name', name: 'Name' },
|
||||||
{ id: 'size', name: 'Size' },
|
{ id: 'date', name: 'Date' },
|
||||||
]}
|
{ id: 'size', name: 'Size' },
|
||||||
rows={exports ? exports.map((x, i) => ({
|
]}
|
||||||
name: <Link href={'/api/user/export?name=' + x.full}>Export {i + 1}</Link>,
|
rows={exports ? exports.map((x, i) => ({
|
||||||
date: x.date.toLocaleString(),
|
name: <Link href={'/api/user/export?name=' + x.full}>Export {i + 1}</Link>,
|
||||||
size: bytesToRead(x.size),
|
date: x.date.toLocaleString(),
|
||||||
})) : []} />
|
size: bytesToRead(x.size),
|
||||||
|
})) : []} />
|
||||||
|
) : (
|
||||||
|
<Text>No exports yet</Text>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Title my='md'>ShareX Config</Title>
|
<Title my='md'>ShareX Config</Title>
|
||||||
|
|
|
@ -43,7 +43,7 @@ export default function Stats() {
|
||||||
</Card>
|
</Card>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
{stats.count_by_user.length ? (
|
{stats && stats.count_by_user.length ? (
|
||||||
<Card name='Files per User' mt={22}>
|
<Card name='Files per User' mt={22}>
|
||||||
<SmallTable
|
<SmallTable
|
||||||
columns={[
|
columns={[
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Button, Collapse, Group, Progress, Title } from '@mantine/core';
|
import { Button, Collapse, Group, Progress, Title } from '@mantine/core';
|
||||||
import { randomId, useClipboard } from '@mantine/hooks';
|
import { randomId, useClipboard } from '@mantine/hooks';
|
||||||
import { useNotifications } from '@mantine/notifications';
|
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||||
import Dropzone from 'components/dropzone/Dropzone';
|
import Dropzone from 'components/dropzone/Dropzone';
|
||||||
import FileDropzone from 'components/dropzone/DropzoneFile';
|
import FileDropzone from 'components/dropzone/DropzoneFile';
|
||||||
import { CrossIcon, UploadIcon } from 'components/icons';
|
import { CrossIcon, UploadIcon } from 'components/icons';
|
||||||
|
@ -9,7 +9,6 @@ import { useStoreSelector } from 'lib/redux/store';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function Upload() {
|
export default function Upload() {
|
||||||
const notif = useNotifications();
|
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
const user = useStoreSelector(state => state.user);
|
const user = useStoreSelector(state => state.user);
|
||||||
|
|
||||||
|
@ -22,7 +21,7 @@ export default function Upload() {
|
||||||
const item = Array.from(e.clipboardData.items).find(x => /^image/.test(x.type));
|
const item = Array.from(e.clipboardData.items).find(x => /^image/.test(x.type));
|
||||||
const file = item.getAsFile();
|
const file = item.getAsFile();
|
||||||
setFiles([...files, file]);
|
setFiles([...files, file]);
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Image imported from clipboard',
|
title: 'Image imported from clipboard',
|
||||||
message: '',
|
message: '',
|
||||||
});
|
});
|
||||||
|
@ -35,7 +34,8 @@ export default function Upload() {
|
||||||
const body = new FormData();
|
const body = new FormData();
|
||||||
for (let i = 0; i !== files.length; ++i) body.append('file', files[i]);
|
for (let i = 0; i !== files.length; ++i) body.append('file', files[i]);
|
||||||
|
|
||||||
const id = notif.showNotification({
|
showNotification({
|
||||||
|
id: 'upload',
|
||||||
title: 'Uploading Images...',
|
title: 'Uploading Images...',
|
||||||
message: '',
|
message: '',
|
||||||
loading: true,
|
loading: true,
|
||||||
|
@ -55,7 +55,8 @@ export default function Upload() {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
||||||
if (json.error === undefined) {
|
if (json.error === undefined) {
|
||||||
notif.updateNotification(id, {
|
updateNotification({
|
||||||
|
id: 'upload',
|
||||||
title: 'Upload Successful',
|
title: 'Upload Successful',
|
||||||
message: <>Copied first image to clipboard! <br />{json.files.map(x => (<Link key={x} href={x}>{x}<br /></Link>))}</>,
|
message: <>Copied first image to clipboard! <br />{json.files.map(x => (<Link key={x} href={x}>{x}<br /></Link>))}</>,
|
||||||
color: 'green',
|
color: 'green',
|
||||||
|
@ -64,7 +65,8 @@ export default function Upload() {
|
||||||
clipboard.copy(json.files[0]);
|
clipboard.copy(json.files[0]);
|
||||||
setFiles([]);
|
setFiles([]);
|
||||||
} else {
|
} else {
|
||||||
notif.updateNotification(id, {
|
updateNotification({
|
||||||
|
id: 'upload',
|
||||||
title: 'Upload Failed',
|
title: 'Upload Failed',
|
||||||
message: json.error,
|
message: json.error,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
|
@ -82,10 +84,10 @@ export default function Upload() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Title mb='md'>Upload Files</Title>
|
<Title mb='md'>Upload Files</Title>
|
||||||
|
|
||||||
<Dropzone loading={loading} onDrop={(f) => setFiles([...files, ...f])}>
|
<Dropzone loading={loading} onDrop={(f) => setFiles([...files, ...f])}>
|
||||||
<Group position='center' spacing='md'>
|
<Group position='center' spacing='md'>
|
||||||
{files.map(file => (<FileDropzone key={randomId()} file={file} />))}
|
{files.map(file => (<FileDropzone key={randomId()} file={file} />))}
|
||||||
</Group>
|
</Group>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Button, Group, LoadingOverlay, Select, Title } from '@mantine/core';
|
import { Button, Group, Select, Title } from '@mantine/core';
|
||||||
import { useNotifications } from '@mantine/notifications';
|
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||||
import CodeInput from 'components/CodeInput';
|
import CodeInput from 'components/CodeInput';
|
||||||
import { TypeIcon, UploadIcon } from 'components/icons';
|
import { TypeIcon, UploadIcon } from 'components/icons';
|
||||||
import Link from 'components/Link';
|
import Link from 'components/Link';
|
||||||
|
@ -8,16 +8,16 @@ import { useStoreSelector } from 'lib/redux/store';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
export default function Upload() {
|
export default function Upload() {
|
||||||
const notif = useNotifications();
|
|
||||||
const user = useStoreSelector(state => state.user);
|
const user = useStoreSelector(state => state.user);
|
||||||
|
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const [lang, setLang] = useState('txt');
|
const [lang, setLang] = useState('txt');
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
const file = new File([value], 'text.' + lang);
|
const file = new File([value], 'text.' + lang);
|
||||||
|
|
||||||
const id = notif.showNotification({
|
showNotification({
|
||||||
|
id: 'upload-text',
|
||||||
title: 'Uploading...',
|
title: 'Uploading...',
|
||||||
message: '',
|
message: '',
|
||||||
loading: true,
|
loading: true,
|
||||||
|
@ -30,7 +30,8 @@ export default function Upload() {
|
||||||
const json = JSON.parse(e.target.response);
|
const json = JSON.parse(e.target.response);
|
||||||
|
|
||||||
if (!json.error) {
|
if (!json.error) {
|
||||||
notif.updateNotification(id, {
|
updateNotification({
|
||||||
|
id: 'upload-text',
|
||||||
title: 'Upload Successful',
|
title: 'Upload Successful',
|
||||||
message: <>Copied first file to clipboard! <br />{json.files.map(x => (<Link key={x} href={x}>{x}<br /></Link>))}</>,
|
message: <>Copied first file to clipboard! <br />{json.files.map(x => (<Link key={x} href={x}>{x}<br /></Link>))}</>,
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { ActionIcon, Button, Card, Group, Modal, SimpleGrid, Skeleton, TextInput, Title } from '@mantine/core';
|
import { ActionIcon, Button, Card, Group, Modal, SimpleGrid, Skeleton, TextInput, Title } from '@mantine/core';
|
||||||
import { useClipboard, useForm } from '@mantine/hooks';
|
import { useClipboard } from '@mantine/hooks';
|
||||||
import { useNotifications } from '@mantine/notifications';
|
import { useForm } from '@mantine/form';
|
||||||
|
import { showNotification } from '@mantine/notifications';
|
||||||
import { CopyIcon, CrossIcon, DeleteIcon, LinkIcon, PlusIcon } from 'components/icons';
|
import { CopyIcon, CrossIcon, DeleteIcon, LinkIcon, PlusIcon } from 'components/icons';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
import { useStoreSelector } from 'lib/redux/store';
|
import { useStoreSelector } from 'lib/redux/store';
|
||||||
|
@ -8,7 +9,6 @@ import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
export default function Urls() {
|
export default function Urls() {
|
||||||
const user = useStoreSelector(state => state.user);
|
const user = useStoreSelector(state => state.user);
|
||||||
const notif = useNotifications();
|
|
||||||
const clipboard = useClipboard();
|
const clipboard = useClipboard();
|
||||||
|
|
||||||
const [urls, setURLS] = useState([]);
|
const [urls, setURLS] = useState([]);
|
||||||
|
@ -23,14 +23,14 @@ export default function Urls() {
|
||||||
const deleteURL = async u => {
|
const deleteURL = async u => {
|
||||||
const url = await useFetch('/api/user/urls', 'DELETE', { id: u.id });
|
const url = await useFetch('/api/user/urls', 'DELETE', { id: u.id });
|
||||||
if (url.error) {
|
if (url.error) {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Failed to delete URL',
|
title: 'Failed to delete URL',
|
||||||
message: url.error,
|
message: url.error,
|
||||||
icon: <DeleteIcon />,
|
icon: <DeleteIcon />,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Deleted URL',
|
title: 'Deleted URL',
|
||||||
message: '',
|
message: '',
|
||||||
icon: <CrossIcon />,
|
icon: <CrossIcon />,
|
||||||
|
@ -43,7 +43,7 @@ export default function Urls() {
|
||||||
|
|
||||||
const copyURL = u => {
|
const copyURL = u => {
|
||||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${u.url}`);
|
clipboard.copy(`${window.location.protocol}//${window.location.host}${u.url}`);
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Copied to clipboard',
|
title: 'Copied to clipboard',
|
||||||
message: '',
|
message: '',
|
||||||
icon: <CopyIcon />,
|
icon: <CopyIcon />,
|
||||||
|
@ -86,14 +86,14 @@ export default function Urls() {
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
|
|
||||||
if (json.error) {
|
if (json.error) {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Failed to create URL',
|
title: 'Failed to create URL',
|
||||||
message: json.error,
|
message: json.error,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
icon: <CrossIcon />,
|
icon: <CrossIcon />,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'URL shortened',
|
title: 'URL shortened',
|
||||||
message: json.url,
|
message: json.url,
|
||||||
color: 'green',
|
color: 'green',
|
||||||
|
@ -128,7 +128,7 @@ export default function Urls() {
|
||||||
|
|
||||||
<Group mb='md'>
|
<Group mb='md'>
|
||||||
<Title>URLs</Title>
|
<Title>URLs</Title>
|
||||||
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}><PlusIcon/></ActionIcon>
|
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}><PlusIcon /></ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<SimpleGrid
|
<SimpleGrid
|
||||||
|
@ -145,7 +145,7 @@ export default function Urls() {
|
||||||
<Title>{url.vanity ?? url.id}</Title>
|
<Title>{url.vanity ?? url.id}</Title>
|
||||||
</Group>
|
</Group>
|
||||||
<Group position='right'>
|
<Group position='right'>
|
||||||
<ActionIcon href={url.url} component='a' target='_blank'><LinkIcon/></ActionIcon>
|
<ActionIcon href={url.url} component='a' target='_blank'><LinkIcon /></ActionIcon>
|
||||||
<ActionIcon aria-label='copy' onClick={() => copyURL(url)}>
|
<ActionIcon aria-label='copy' onClick={() => copyURL(url)}>
|
||||||
<CopyIcon />
|
<CopyIcon />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { ActionIcon, Avatar, Button, Card, Group, Modal, SimpleGrid, Skeleton, Switch, TextInput, Title } from '@mantine/core';
|
import { ActionIcon, Avatar, Button, Card, Group, Modal, SimpleGrid, Skeleton, Stack, Switch, Text, TextInput, Title } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/hooks';
|
import { useForm } from '@mantine/form';
|
||||||
import { useModals } from '@mantine/modals';
|
import { useModals } from '@mantine/modals';
|
||||||
import { useNotifications } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
import { CrossIcon, DeleteIcon, PlusIcon } from 'components/icons';
|
import { CrossIcon, DeleteIcon, PlusIcon } from 'components/icons';
|
||||||
|
import MutedText from 'components/MutedText';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
import { useStoreSelector } from 'lib/redux/store';
|
import { useStoreSelector } from 'lib/redux/store';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
@ -17,7 +18,6 @@ function CreateUserModal({ open, setOpen, updateUsers }) {
|
||||||
administrator: false,
|
administrator: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const notif = useNotifications();
|
|
||||||
|
|
||||||
const onSubmit = async values => {
|
const onSubmit = async values => {
|
||||||
const cleanUsername = values.username.trim();
|
const cleanUsername = values.username.trim();
|
||||||
|
@ -34,14 +34,14 @@ function CreateUserModal({ open, setOpen, updateUsers }) {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
const res = await useFetch('/api/auth/create', 'POST', data);
|
const res = await useFetch('/api/auth/create', 'POST', data);
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Failed to create user',
|
title: 'Failed to create user',
|
||||||
message: res.error,
|
message: res.error,
|
||||||
icon: <DeleteIcon />,
|
icon: <DeleteIcon />,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Created user: ' + cleanUsername,
|
title: 'Created user: ' + cleanUsername,
|
||||||
message: '',
|
message: '',
|
||||||
icon: <PlusIcon />,
|
icon: <PlusIcon />,
|
||||||
|
@ -75,7 +75,6 @@ function CreateUserModal({ open, setOpen, updateUsers }) {
|
||||||
export default function Users() {
|
export default function Users() {
|
||||||
const user = useStoreSelector(state => state.user);
|
const user = useStoreSelector(state => state.user);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const notif = useNotifications();
|
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
|
||||||
const [users, setUsers] = useState([]);
|
const [users, setUsers] = useState([]);
|
||||||
|
@ -87,14 +86,14 @@ export default function Users() {
|
||||||
delete_images,
|
delete_images,
|
||||||
});
|
});
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Failed to delete user',
|
title: 'Failed to delete user',
|
||||||
message: res.error,
|
message: res.error,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
icon: <CrossIcon />,
|
icon: <CrossIcon />,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'User deleted',
|
title: 'User deleted',
|
||||||
message: '',
|
message: '',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
|
@ -108,8 +107,6 @@ export default function Users() {
|
||||||
const openDeleteModal = user => modals.openConfirmModal({
|
const openDeleteModal = user => modals.openConfirmModal({
|
||||||
title: `Delete ${user.username}?`,
|
title: `Delete ${user.username}?`,
|
||||||
closeOnConfirm: false,
|
closeOnConfirm: false,
|
||||||
centered: true,
|
|
||||||
overlayBlur: 3,
|
|
||||||
labels: { confirm: 'Yes', cancel: 'No' },
|
labels: { confirm: 'Yes', cancel: 'No' },
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
modals.openConfirmModal({
|
modals.openConfirmModal({
|
||||||
|
@ -160,8 +157,12 @@ export default function Users() {
|
||||||
<Card key={user.id} sx={{ maxWidth: '100%' }}>
|
<Card key={user.id} sx={{ maxWidth: '100%' }}>
|
||||||
<Group position='apart'>
|
<Group position='apart'>
|
||||||
<Group position='left'>
|
<Group position='left'>
|
||||||
<Avatar color={user.administrator ? 'primary' : 'dark'}>{user.username[0]}</Avatar>
|
<Avatar size='lg' color={user.administrator ? 'primary' : 'dark'}>{user.username[0]}</Avatar>
|
||||||
<Title>{user.username}</Title>
|
<Stack spacing={0}>
|
||||||
|
<Title>{user.username}</Title>
|
||||||
|
<MutedText size='sm'>ID: {user.id}</MutedText>
|
||||||
|
<MutedText size='sm'>Administrator: {user.administrator ? 'yes' : 'no'}</MutedText>
|
||||||
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
<Group position='right'>
|
<Group position='right'>
|
||||||
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(user)}>
|
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(user)}>
|
||||||
|
|
|
@ -176,7 +176,6 @@ export class Swift extends Datasource {
|
||||||
|
|
||||||
public constructor(public config: ConfigSwiftDatasource) {
|
public constructor(public config: ConfigSwiftDatasource) {
|
||||||
super();
|
super();
|
||||||
console.log(config);
|
|
||||||
this.container = new SwiftContainer({
|
this.container = new SwiftContainer({
|
||||||
auth_endpoint_url: config.auth_endpoint,
|
auth_endpoint_url: config.auth_endpoint,
|
||||||
credentials: {
|
credentials: {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Button, Center, TextInput, Title, PasswordInput } from '@mantine/core';
|
import { Button, Center, TextInput, Title, PasswordInput } from '@mantine/core';
|
||||||
import { useForm } from '@mantine/hooks';
|
import { useForm } from '@mantine/form';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { useState } from 'react';
|
||||||
import { Button, Card, Center, Group, PasswordInput, Stepper, TextInput } from '@mantine/core';
|
import { Button, Card, Center, Group, PasswordInput, Stepper, TextInput } from '@mantine/core';
|
||||||
import useFetch from 'hooks/useFetch';
|
import useFetch from 'hooks/useFetch';
|
||||||
import PasswordStrength from 'components/PasswordStrength';
|
import PasswordStrength from 'components/PasswordStrength';
|
||||||
import { useNotifications } from '@mantine/notifications';
|
import { showNotification } from '@mantine/notifications';
|
||||||
import { CrossIcon, UserIcon } from 'components/icons';
|
import { CrossIcon, UserIcon } from 'components/icons';
|
||||||
import { useStoreDispatch } from 'lib/redux/store';
|
import { useStoreDispatch } from 'lib/redux/store';
|
||||||
import { updateUser } from 'lib/redux/reducers/user';
|
import { updateUser } from 'lib/redux/reducers/user';
|
||||||
|
@ -20,7 +20,6 @@ export default function Invite({ code, title }) {
|
||||||
const [verifyPasswordError, setVerifyPasswordError] = useState('');
|
const [verifyPasswordError, setVerifyPasswordError] = useState('');
|
||||||
const [strength, setStrength] = useState(0);
|
const [strength, setStrength] = useState(0);
|
||||||
|
|
||||||
const notif = useNotifications();
|
|
||||||
const dispatch = useStoreDispatch();
|
const dispatch = useStoreDispatch();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
@ -53,14 +52,14 @@ export default function Invite({ code, title }) {
|
||||||
const createUser = async () => {
|
const createUser = async () => {
|
||||||
const res = await useFetch('/api/auth/create', 'POST', { code, username, password });
|
const res = await useFetch('/api/auth/create', 'POST', { code, username, password });
|
||||||
if (res.error) {
|
if (res.error) {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'Error while creating user',
|
title: 'Error while creating user',
|
||||||
message: res.error,
|
message: res.error,
|
||||||
color: 'red',
|
color: 'red',
|
||||||
icon: <CrossIcon />,
|
icon: <CrossIcon />,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notif.showNotification({
|
showNotification({
|
||||||
title: 'User created',
|
title: 'User created',
|
||||||
message: 'You will be logged in shortly...',
|
message: 'You will be logged in shortly...',
|
||||||
color: 'green',
|
color: 'green',
|
||||||
|
@ -74,7 +73,7 @@ export default function Invite({ code, title }) {
|
||||||
});
|
});
|
||||||
|
|
||||||
router.push('/dashboard');
|
router.push('/dashboard');
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,11 @@ import { version } from '../../package.json';
|
||||||
import type { Config } from 'lib/config/Config';
|
import type { Config } from 'lib/config/Config';
|
||||||
import type { Datasource } from 'lib/datasources';
|
import type { Datasource } from 'lib/datasources';
|
||||||
|
|
||||||
|
const dev = process.env.NODE_ENV === 'development';
|
||||||
let config: Config, datasource: Datasource;
|
let config: Config, datasource: Datasource;
|
||||||
|
|
||||||
const logger = Logger.get('server');
|
const logger = Logger.get('server');
|
||||||
logger.info(`starting zipline@${version} server`);
|
logger.info(`starting ${process.env.NODE_ENV || 'production'} zipline@${version} server`);
|
||||||
|
|
||||||
start();
|
start();
|
||||||
|
|
||||||
|
@ -40,8 +41,6 @@ async function start() {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const dev = process.env.NODE_ENV === 'development';
|
|
||||||
|
|
||||||
process.env.DATABASE_URL = config.core.database_url;
|
process.env.DATABASE_URL = config.core.database_url;
|
||||||
await migrations();
|
await migrations();
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue