feat: switch to mantine v5

This commit is contained in:
diced 2022-07-28 11:03:22 -07:00
parent 12baadd563
commit 3ea24ddf0c
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
26 changed files with 1196 additions and 1149 deletions

0
.yarn/releases/yarn-3.2.1.cjs vendored Normal file → Executable file
View file

View 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,
}); });
})(); })();

View file

@ -8,9 +8,6 @@ module.exports = {
}, },
]; ];
}, },
api: {
responseLimit: false,
},
poweredByHeader: false, poweredByHeader: false,
reactStrictMode: true, reactStrictMode: true,
}; };

View file

@ -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",

View file

@ -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

View file

@ -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&apos;t share this token with anyone as they will be able to upload files on your behalf. Make sure you don&apos;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>
); );
} }

View file

@ -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>;
} }

View file

@ -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>
); );
} }

View file

@ -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',
},
}, },
}, },
}} }}

View file

@ -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} />
); );

View file

@ -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>
); );
} }

View file

@ -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>

View file

@ -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 },

View file

@ -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}

View file

@ -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();
}, []); }, []);

View file

@ -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>

View file

@ -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={[

View file

@ -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>

View file

@ -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>))}</>,
}); });

View file

@ -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>

View file

@ -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)}>

View file

@ -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: {

View file

@ -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';

View file

@ -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');
} }
}; };

View file

@ -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();

1614
yarn.lock

File diff suppressed because it is too large Load diff