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,
watch,
incremental: watch,
sourcemap: false,
sourcemap: true,
minify: false,
});
})();

View file

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

View file

@ -3,7 +3,7 @@
"version": "3.4.8",
"license": "MIT",
"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:server": "node esbuild.config.js",
"build:next": "next build",
@ -16,17 +16,21 @@
"docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build"
},
"dependencies": {
"@emotion/react": "^11.9.3",
"@emotion/server": "^11.4.0",
"@iarna/toml": "2.2.5",
"@mantine/core": "^4.2.9",
"@mantine/dropzone": "^4.2.9",
"@mantine/hooks": "^4.2.9",
"@mantine/modals": "^4.2.9",
"@mantine/next": "^4.2.9",
"@mantine/notifications": "^4.2.9",
"@mantine/prism": "^4.2.9",
"@prisma/client": "^3.15.2",
"@prisma/migrate": "^3.15.2",
"@prisma/sdk": "^3.15.2",
"@mantine/core": "^5.0.0",
"@mantine/dropzone": "^5.0.0",
"@mantine/form": "^5.0.0",
"@mantine/hooks": "^5.0.0",
"@mantine/modals": "^5.0.0",
"@mantine/next": "^5.0.0",
"@mantine/notifications": "^5.0.0",
"@mantine/nprogress": "^5.0.0",
"@mantine/prism": "^5.0.0",
"@prisma/client": "^4.1.0",
"@prisma/internals": "^4.1.0",
"@prisma/migrate": "^4.1.0",
"@reduxjs/toolkit": "^1.8.2",
"argon2": "^0.28.5",
"colorette": "^2.0.19",
@ -39,7 +43,7 @@
"minio": "^7.0.28",
"multer": "^1.4.5-lts.1",
"next": "^12.1.6",
"prisma": "^3.15.2",
"prisma": "^4.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"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 { useNotifications } from '@mantine/notifications';
import { showNotification } from '@mantine/notifications';
import useFetch from 'hooks/useFetch';
import { useState } from 'react';
import Type from './Type';
@ -21,7 +21,6 @@ export function FileMeta({ Icon, title, subtitle }) {
export default function File({ image, updateImages }) {
const [open, setOpen] = useState(false);
const notif = useNotifications();
const clipboard = useClipboard();
const theme = useMantineTheme();
@ -29,14 +28,14 @@ export default function File({ image, updateImages }) {
const res = await useFetch('/api/user/files', 'DELETE', { id: image.id });
if (!res.error) {
updateImages(true);
notif.showNotification({
showNotification({
title: 'File Deleted',
message: '',
color: 'green',
icon: <DeleteIcon />,
});
} else {
notif.showNotification({
showNotification({
title: 'Failed to delete file',
message: res.error,
color: 'red',
@ -50,7 +49,7 @@ export default function File({ image, updateImages }) {
const handleCopy = () => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${image.url}`);
setOpen(false);
notif.showNotification({
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <CopyIcon />,
@ -60,7 +59,7 @@ export default function File({ image, updateImages }) {
const handleFavorite = async () => {
const data = await useFetch('/api/user/files', 'PATCH', { id: image.id, favorite: !image.favorite });
if (!data.error) updateImages(true);
notif.showNotification({
showNotification({
title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'),
message: '',
icon: <StarIcon />,
@ -75,8 +74,6 @@ export default function File({ image, updateImages }) {
onClose={() => setOpen(false)}
title={<Title>{image.file}</Title>}
size='xl'
overlayBlur={3}
overlayColor={theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white'}
>
<Stack>
<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 { useModals } from '@mantine/modals';
import { useNotifications } from '@mantine/notifications';
import { showNotification } from '@mantine/notifications';
import useFetch from 'hooks/useFetch';
import { updateUser } from 'lib/redux/reducers/user';
import { useStoreDispatch } from 'lib/redux/store';
@ -64,37 +64,50 @@ function MenuItem(props) {
const items = [
{
icon: <HomeIcon />,
icon: <HomeIcon size={18} />,
text: 'Home',
link: '/dashboard',
},
{
icon: <FileIcon />,
icon: <FileIcon size={18} />,
text: 'Files',
link: '/dashboard/files',
},
{
icon: <ActivityIcon />,
icon: <ActivityIcon size={18} />,
text: 'Stats',
link: '/dashboard/stats',
},
{
icon: <LinkIcon />,
icon: <LinkIcon size={18} />,
text: 'URLs',
link: '/dashboard/urls',
},
{
icon: <UploadIcon />,
icon: <UploadIcon size={18} />,
text: 'Upload',
link: '/dashboard/upload',
},
{
icon: <TypeIcon />,
icon: <TypeIcon size={18} />,
text: 'Upload 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 }) {
const [token, setToken] = useState(user?.token);
const [systemTheme, setSystemTheme] = useState(user.systemTheme ?? 'system');
@ -104,7 +117,6 @@ export default function Layout({ children, user, title }) {
const dispatch = useStoreDispatch();
const theme = useMantineTheme();
const modals = useModals();
const notif = useNotifications();
const clipboard = useClipboard();
const handleUpdateTheme = async value => {
@ -116,7 +128,7 @@ export default function Layout({ children, user, title }) {
dispatch(updateUser(newUser));
router.replace(router.pathname);
notif.showNotification({
showNotification({
title: `Theme changed to ${friendlyThemeName[value]}`,
message: '',
color: 'green',
@ -126,8 +138,6 @@ export default function Layout({ children, user, title }) {
const openResetToken = () => modals.openConfirmModal({
title: 'Reset Token',
centered: true,
overlayBlur: 3,
children: (
<Text size='sm'>
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');
if (!a.success) {
setToken(a.success);
notif.showNotification({
showNotification({
title: 'Token Reset Failed',
message: a.error,
color: 'red',
icon: <CrossIcon />,
});
} else {
notif.showNotification({
showNotification({
title: 'Token Reset',
message: 'Your token has been reset. You will need to update any uploaders to use this new token.',
color: 'green',
@ -159,8 +169,6 @@ export default function Layout({ children, user, title }) {
const openCopyToken = () => modals.openConfirmModal({
title: 'Copy Token',
centered: true,
overlayBlur: 3,
children: (
<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.
@ -170,7 +178,7 @@ export default function Layout({ children, user, title }) {
onConfirm: async () => {
clipboard.copy(token);
notif.showNotification({
showNotification({
title: 'Token Copied',
message: 'Your token has been copied to your clipboard.',
color: 'green',
@ -187,7 +195,7 @@ export default function Layout({ children, user, title }) {
fixed
navbar={
<Navbar
p='md'
pt='sm'
hiddenBreakpoint='sm'
hidden={!opened}
width={{ sm: 200, lg: 230 }}
@ -195,84 +203,37 @@ export default function Layout({ children, user, title }) {
<Navbar.Section
grow
component={ScrollArea}
ml={-10}
mr={-10}
sx={{ paddingLeft: 10, paddingRight: 10 }}
>
{items.map(({ icon, text, link }) => (
<Link href={link} key={text} 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'>
{icon}
</ThemeIcon>
<Text size='lg'>{text}</Text>
</Group>
</UnstyledButton>
<NavLink
component='a'
label={text}
icon={icon}
active={router.pathname === link}
variant='light'
/>
</Link>
))}
{user.administrator && (
<>
<Link href='/dashboard/users' 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'>
<UserIcon />
</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>
</>
<NavLink
label='Administration'
icon={<SettingsIcon />}
childrenOffset={28}
defaultOpened={admin_items.map(x => x.link).includes(router.pathname)}
>
{admin_items.map(({ icon, text, link }) => (
<Link href={link} key={text} passHref>
<NavLink
component='a'
label={text}
icon={icon}
active={router.pathname === link}
variant='light'
/>
</Link>
))}
</NavLink>
)}
</Navbar.Section>
</Navbar>
@ -291,75 +252,78 @@ export default function Layout({ children, user, title }) {
<Title ml='md'>{title}</Title>
<Box sx={{ marginLeft: 'auto', marginRight: 0 }}>
<Popover
position='top'
placement='end'
spacing={4}
position='bottom-end'
opened={open}
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}>
<Text sx={{
color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
fontWeight: 500,
fontSize: theme.fontSizes.sm,
padding: `${theme.spacing.xs / 2}px ${theme.spacing.sm}px`,
cursor: 'default',
}}
<Popover.Target>
<Button
leftIcon={<SettingsIcon />}
onClick={() => setOpen((o) => !o)}
sx={t => ({
backgroundColor: '#00000000',
'&:hover': {
backgroundColor: t.other.hover,
},
})}
size='xl'
p='sm'
>
{user.username}
</Text>
<MenuItemLink icon={<SettingsIcon />} href='/dashboard/manage'>Manage Account</MenuItemLink>
<MenuItem icon={<CopyIcon />} onClick={() => {setOpen(false);openCopyToken();}}>Copy Token</MenuItem>
<MenuItem icon={<DeleteIcon />} onClick={() => {setOpen(false);openResetToken();}} color='red'>Reset Token</MenuItem>
<MenuItemLink icon={<LogoutIcon />} href='/auth/logout' color='red'>Logout</MenuItemLink>
<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 icon={<PencilIcon />}>
<Select
size='xs'
data={Object.keys(themes).map(t => ({ value: t, label: friendlyThemeName[t] }))}
value={systemTheme}
onChange={handleUpdateTheme}
</Button>
</Popover.Target>
<Popover.Dropdown p={4}>
<Stack spacing={2}>
<Text sx={{
color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
fontWeight: 500,
fontSize: theme.fontSizes.sm,
padding: `${theme.spacing.xs / 2}px ${theme.spacing.sm}px`,
cursor: 'default',
}}
>
{user.username}
</Text>
<MenuItemLink icon={<SettingsIcon />} href='/dashboard/manage'>Manage Account</MenuItemLink>
<MenuItem icon={<CopyIcon />} onClick={() => {setOpen(false);openCopyToken();}}>Copy Token</MenuItem>
<MenuItem icon={<DeleteIcon />} onClick={() => {setOpen(false);openResetToken();}} color='red'>Reset Token</MenuItem>
<MenuItemLink icon={<LogoutIcon />} href='/auth/logout' color='red'>Logout</MenuItemLink>
<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>
</Group>
<MenuItem icon={<PencilIcon />}>
<Select
size='xs'
data={Object.keys(themes).map(t => ({ value: t, label: friendlyThemeName[t] }))}
value={systemTheme}
onChange={handleUpdateTheme}
/>
</MenuItem>
</Stack>
</Popover.Dropdown>
</Popover>
</Box>
</div>
</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>
);
}

View file

@ -1,5 +1,5 @@
import { Text } from '@mantine/core';
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 (
<Popover
opened={popoverOpened}
position='bottom'
placement='start'
position='top'
width='target'
withArrow
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' />
<PasswordRequirement label='Includes at least 8 characters' meets={value.length > 7} />
{checks}
<Popover.Target>
<div
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>
);
}

View file

@ -64,24 +64,34 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
<MantineProvider
withGlobalStyles
withNormalizeCSS
theme={theme}
styles={{
AppShell: t => ({
root: {
backgroundColor: t.other.AppShell_backgroundColor,
theme={{
...theme,
components: {
AppShell: {
styles: t => ({
root: {
backgroundColor: t.other.AppShell_backgroundColor,
},
}),
},
}),
Popover: {
inner: {
width: 200,
NavLink: {
styles: t => ({
icon: {
paddingLeft: t.spacing.sm,
},
}),
},
},
Accordion: {
itemTitle: {
border: 0,
Modal: {
defaultProps: {
centered: true,
overlayBlur: 3,
overlayColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
},
},
itemOpened: {
border: 0,
Popover: {
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 { useEffect, useState } from 'react';
import { AudioIcon, FileIcon, PlayIcon, TypeIcon, VideoIcon } from './icons';
import MutedText from './MutedText';
import { AudioIcon, FileIcon, PlayIcon } from './icons';
function Placeholder({ text, Icon, ...props }) {
return (
<Image height={200} withPlaceholder placeholder={
<Group>
<Icon size={48} />
<Text>{text}</Text>
<Text size='md'>{text}</Text>
</Group>
} {...props} />
);

View file

@ -1,52 +1,24 @@
import React from 'react';
import { Dropzone as MantineDropzone } from '@mantine/dropzone';
import { Group, Text, useMantineTheme } from '@mantine/core';
import { CrossIcon, UploadIcon, 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;
}
import { ImageIcon } from 'components/icons';
export default function Dropzone({ loading, onDrop, children }) {
const theme = useMantineTheme();
return (
<MantineDropzone loading={loading} onDrop={onDrop}>
{status => (
<>
<Group position='center' spacing='xl' style={{ minHeight: 440, pointerEvents: 'none' }}>
<ImageUploadIcon
status={status}
style={{ width: 80, height: 80, color: getIconColor(status, theme) }}
/>
<MantineDropzone onDrop={onDrop}>
<Group position='center' spacing='xl' style={{ minHeight: 440 }}>
<ImageIcon size={80} />
<Text size='xl' inline>
Drag images here or click to select files
</Text>
</Group>
<Text size='xl' inline>
Drag images here or click to select files
</Text>
</Group>
{children}
</>
)}
<div style={{ pointerEvents: 'all' }}>
{children}
</div>
</MantineDropzone>
);
}

View file

@ -11,6 +11,7 @@ export function FilePreview({ file }: { file: File }) {
style={{ maxWidth: '10vw', maxHeight: '100vh' }}
src={URL.createObjectURL(file)}
alt={file.name}
popup
/>
);
}
@ -21,13 +22,11 @@ export default function FileDropzone({ file }: { file: File }) {
return (
<Tooltip
position='top'
placement='center'
allowPointerEvents
label={
<div style={{ display: 'flex', alignItems: 'center' }}>
<FilePreview file={file} />
<Table sx={{ color: theme.colorScheme === 'dark' ? 'black' : 'white' }} ml='md'>
<Table sx={{ color: theme.colorScheme === 'dark' ? 'white' : 'white' }} ml='md'>
<tbody>
<tr>
<td>Name</td>

View file

@ -1,6 +1,6 @@
import { SimpleGrid, Skeleton, Text, Title } from '@mantine/core';
import { randomId, useClipboard } from '@mantine/hooks';
import { useNotifications } from '@mantine/notifications';
import { showNotification } from '@mantine/notifications';
import Card from 'components/Card';
import File from 'components/File';
import { CopyIcon, CrossIcon, DeleteIcon } from 'components/icons';
@ -21,7 +21,6 @@ export default function Dashboard() {
const [recent, setRecent] = useState([]);
const [stats, setStats] = useState(null);
const clipboard = useClipboard();
const notif = useNotifications();
const updateImages = async () => {
const imgs = await useFetch('/api/user/files');
@ -36,14 +35,14 @@ export default function Dashboard() {
const res = await useFetch('/api/user/files', 'DELETE', { id: original.id });
if (!res.error) {
updateImages();
notif.showNotification({
showNotification({
title: 'Image Deleted',
message: '',
color: 'green',
icon: <DeleteIcon />,
});
} else {
notif.showNotification({
showNotification({
title: 'Failed to delete image',
message: res.error,
color: 'red',
@ -55,7 +54,7 @@ export default function Dashboard() {
const copyImage = async ({ original }) => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${original.url}`);
notif.showNotification({
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <CopyIcon />,
@ -73,7 +72,7 @@ export default function Dashboard() {
return (
<>
<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>
<SimpleGrid
@ -85,15 +84,15 @@ export default function Dashboard() {
>
{recent.length ? recent.map(image => (
<File key={randomId()} image={image} updateImages={updateImages} />
)) : [1,2,3,4].map(x => (
)) : [1, 2, 3, 4].map(x => (
<div key={x}>
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }}/>
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }} />
</div>
))}
</SimpleGrid>
<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
cols={3}
spacing='lg'
@ -117,7 +116,7 @@ export default function Dashboard() {
</SimpleGrid>
<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
columns={[
{ accessor: 'file', Header: 'Name', minWidth: 170, align: 'inherit' as Aligns },

View file

@ -34,39 +34,37 @@ export default function Files() {
</Group>
{favoritePages.length ? (
<Accordion
offsetIcon={false}
sx={t => ({
marginTop: 2,
border: '1px solid',
marginBottom: 12,
borderColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0] ,
})}
variant='contained'
mb='sm'
>
<Accordion.Item label={<Title>Favorite Files</Title>}>
<SimpleGrid
cols={3}
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)} />
</div>
)) : null}
</SimpleGrid>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
paddingTop: 12,
paddingBottom: 3,
}}
>
<Pagination total={favoritePages.length} page={favoritePage} onChange={setFavoritePage}/>
</Box>
<Accordion.Item value='favorite'>
<Accordion.Control>Favorite Files</Accordion.Control>
<Accordion.Panel>
<SimpleGrid
cols={3}
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)} />
</div>
)) : null}
</SimpleGrid>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
paddingTop: 12,
paddingBottom: 3,
}}
>
<Pagination total={favoritePages.length} page={favoritePage} onChange={setFavoritePage}/>
</Box>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
) : 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 { useClipboard, useForm } from '@mantine/hooks';
import { useClipboard } from '@mantine/hooks';
import { useForm } from '@mantine/form';
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 MutedText from 'components/MutedText';
import useFetch from 'hooks/useFetch';
@ -26,7 +27,6 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
expires: '30m',
},
});
const notif = useNotifications();
const onSubmit = async values => {
if (!expires.includes(values.expires)) return form.setFieldError('expires', 'Invalid expiration');
@ -47,14 +47,14 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
});
if (res.error) {
notif.showNotification({
showNotification({
title: 'Failed to create invite',
message: res.error,
icon: <CrossIcon />,
color: 'red',
});
} else {
notif.showNotification({
showNotification({
title: 'Created invite',
message: '',
icon: <TagIcon />,
@ -70,9 +70,6 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
opened={open}
onClose={() => setOpen(false)}
title={<Title>Create Invite</Title>}
overlayBlur={3}
centered={true}
>
<form onSubmit={form.onSubmit(v => onSubmit(v))}>
<Select
@ -103,7 +100,6 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
export default function Users() {
const router = useRouter();
const notif = useNotifications();
const modals = useModals();
const clipboard = useClipboard();
@ -118,14 +114,14 @@ export default function Users() {
onConfirm: async () => {
const res = await useFetch(`/api/auth/invite?code=${invite.code}`, 'DELETE');
if (res.error) {
notif.showNotification({
showNotification({
title: 'Failed to delete invite ${invite.code}',
message: res.error,
icon: <CrossIcon />,
color: 'red',
});
} else {
notif.showNotification({
showNotification({
title: `Deleted invite ${invite.code}`,
message: '',
icon: <DeleteIcon />,
@ -139,7 +135,7 @@ export default function Users() {
const handleCopy = async invite => {
clipboard.copy(`${window.location.protocol}//${window.location.host}/invite/${invite.code}`);
notif.showNotification({
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <CopyIcon />,
@ -156,7 +152,6 @@ export default function Users() {
};
useEffect(() => {
console.log(invites);
updateInvites();
}, []);

View file

@ -1,7 +1,8 @@
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 { useNotifications } from '@mantine/notifications';
import { showNotification, updateNotification } from '@mantine/notifications';
import { CrossIcon, DeleteIcon } from 'components/icons';
import DownloadIcon from 'components/icons/DownloadIcon';
import Link from 'components/Link';
@ -11,31 +12,15 @@ import { bytesToRead } from 'lib/clientUtils';
import { updateUser } from 'lib/redux/reducers/user';
import { useStoreDispatch, useStoreSelector } from 'lib/redux/store';
import { useEffect, useState } from 'react';
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>
);
}
import MutedText from 'components/MutedText';
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() {
const user = useStoreSelector(state => state.user);
const dispatch = useStoreDispatch();
const notif = useNotifications();
const modals = useModals();
const [exports, setExports] = useState([]);
@ -87,7 +72,8 @@ export default function Manage() {
if (cleanUsername === '') return form.setFieldError('username', 'Username can\'t be nothing');
const id = notif.showNotification({
showNotification({
id: 'update-user',
title: 'Updating user...',
message: '',
loading: true,
@ -107,7 +93,8 @@ export default function Manage() {
if (newUser.error) {
if (newUser.invalidDomains) {
notif.updateNotification(id, {
updateNotification({
id: 'update-user',
message: <>
<Text mt='xs'>The following domains are invalid:</Text>
{newUser.invalidDomains.map(err => (
@ -121,7 +108,8 @@ export default function Manage() {
icon: <CrossIcon />,
});
}
notif.updateNotification(id, {
updateNotification({
id: 'update-user',
title: 'Couldn\'t save user',
message: newUser.error,
color: 'red',
@ -129,7 +117,8 @@ export default function Manage() {
});
} else {
dispatch(updateUser(newUser));
notif.updateNotification(id, {
updateNotification({
id: 'update-user',
title: 'Saved User',
message: '',
});
@ -139,7 +128,7 @@ export default function Manage() {
const exportData = async () => {
const res = await useFetch('/api/user/export', 'POST');
if (res.url) {
notif.showNotification({
showNotification({
title: 'Export started...',
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.',
@ -163,14 +152,14 @@ export default function Manage() {
});
if (!res.count) {
notif.showNotification({
showNotification({
title: 'Couldn\'t delete files',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
} else {
notif.showNotification({
showNotification({
title: 'Deleted files',
message: `${res.count} files deleted`,
color: 'green',
@ -182,14 +171,10 @@ export default function Manage() {
const openDeleteModal = () => modals.openConfirmModal({
title: 'Are you sure you want to delete all of your images?',
closeOnConfirm: false,
centered: true,
overlayBlur: 3,
labels: { confirm: 'Yes', cancel: 'No' },
onConfirm: () => {
modals.openConfirmModal({
title: 'Are you really sure?',
centered: true,
overlayBlur: 3,
labels: { confirm: 'Yes', cancel: 'No' },
onConfirm: () => {
handleDelete();
@ -211,9 +196,7 @@ export default function Manage() {
return (
<>
<Title>Manage User</Title>
<VarsTooltip>
<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>
<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>
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
<TextInput id='username' label='Username' {...form.getInputProps('username')} />
<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'>
<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>
<Group>
@ -250,17 +233,21 @@ export default function Manage() {
<ExportDataTooltip><Button onClick={exportData} rightIcon={<DownloadIcon />}>Export Data</Button></ExportDataTooltip>
</Group>
<Card mt={22}>
<SmallTable
columns={[
{ id: 'name', name: 'Name' },
{ 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>,
date: x.date.toLocaleString(),
size: bytesToRead(x.size),
})) : []} />
{exports && exports.length ? (
<SmallTable
columns={[
{ id: 'name', name: 'Name' },
{ 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>,
date: x.date.toLocaleString(),
size: bytesToRead(x.size),
})) : []} />
) : (
<Text>No exports yet</Text>
)}
</Card>
<Title my='md'>ShareX Config</Title>

View file

@ -43,7 +43,7 @@ export default function Stats() {
</Card>
</SimpleGrid>
{stats.count_by_user.length ? (
{stats && stats.count_by_user.length ? (
<Card name='Files per User' mt={22}>
<SmallTable
columns={[

View file

@ -1,6 +1,6 @@
import { Button, Collapse, Group, Progress, Title } from '@mantine/core';
import { randomId, useClipboard } from '@mantine/hooks';
import { useNotifications } from '@mantine/notifications';
import { showNotification, updateNotification } from '@mantine/notifications';
import Dropzone from 'components/dropzone/Dropzone';
import FileDropzone from 'components/dropzone/DropzoneFile';
import { CrossIcon, UploadIcon } from 'components/icons';
@ -9,7 +9,6 @@ import { useStoreSelector } from 'lib/redux/store';
import { useEffect, useState } from 'react';
export default function Upload() {
const notif = useNotifications();
const clipboard = useClipboard();
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 file = item.getAsFile();
setFiles([...files, file]);
notif.showNotification({
showNotification({
title: 'Image imported from clipboard',
message: '',
});
@ -35,7 +34,8 @@ export default function Upload() {
const body = new FormData();
for (let i = 0; i !== files.length; ++i) body.append('file', files[i]);
const id = notif.showNotification({
showNotification({
id: 'upload',
title: 'Uploading Images...',
message: '',
loading: true,
@ -55,7 +55,8 @@ export default function Upload() {
setLoading(false);
if (json.error === undefined) {
notif.updateNotification(id, {
updateNotification({
id: 'upload',
title: 'Upload Successful',
message: <>Copied first image to clipboard! <br />{json.files.map(x => (<Link key={x} href={x}>{x}<br /></Link>))}</>,
color: 'green',
@ -64,7 +65,8 @@ export default function Upload() {
clipboard.copy(json.files[0]);
setFiles([]);
} else {
notif.updateNotification(id, {
updateNotification({
id: 'upload',
title: 'Upload Failed',
message: json.error,
color: 'red',
@ -85,7 +87,7 @@ export default function Upload() {
<Dropzone loading={loading} onDrop={(f) => setFiles([...files, ...f])}>
<Group position='center' spacing='md'>
{files.map(file => (<FileDropzone key={randomId()} file={file} />))}
{files.map(file => (<FileDropzone key={randomId()} file={file} />))}
</Group>
</Dropzone>

View file

@ -1,5 +1,5 @@
import { Button, Group, LoadingOverlay, Select, Title } from '@mantine/core';
import { useNotifications } from '@mantine/notifications';
import { Button, Group, Select, Title } from '@mantine/core';
import { showNotification, updateNotification } from '@mantine/notifications';
import CodeInput from 'components/CodeInput';
import { TypeIcon, UploadIcon } from 'components/icons';
import Link from 'components/Link';
@ -8,7 +8,6 @@ import { useStoreSelector } from 'lib/redux/store';
import { useState } from 'react';
export default function Upload() {
const notif = useNotifications();
const user = useStoreSelector(state => state.user);
const [value, setValue] = useState('');
@ -17,7 +16,8 @@ export default function Upload() {
const handleUpload = async () => {
const file = new File([value], 'text.' + lang);
const id = notif.showNotification({
showNotification({
id: 'upload-text',
title: 'Uploading...',
message: '',
loading: true,
@ -30,7 +30,8 @@ export default function Upload() {
const json = JSON.parse(e.target.response);
if (!json.error) {
notif.updateNotification(id, {
updateNotification({
id: 'upload-text',
title: 'Upload Successful',
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 { useClipboard, useForm } from '@mantine/hooks';
import { useNotifications } from '@mantine/notifications';
import { useClipboard } from '@mantine/hooks';
import { useForm } from '@mantine/form';
import { showNotification } from '@mantine/notifications';
import { CopyIcon, CrossIcon, DeleteIcon, LinkIcon, PlusIcon } from 'components/icons';
import useFetch from 'hooks/useFetch';
import { useStoreSelector } from 'lib/redux/store';
@ -8,7 +9,6 @@ import { useEffect, useState } from 'react';
export default function Urls() {
const user = useStoreSelector(state => state.user);
const notif = useNotifications();
const clipboard = useClipboard();
const [urls, setURLS] = useState([]);
@ -23,14 +23,14 @@ export default function Urls() {
const deleteURL = async u => {
const url = await useFetch('/api/user/urls', 'DELETE', { id: u.id });
if (url.error) {
notif.showNotification({
showNotification({
title: 'Failed to delete URL',
message: url.error,
icon: <DeleteIcon />,
color: 'red',
});
} else {
notif.showNotification({
showNotification({
title: 'Deleted URL',
message: '',
icon: <CrossIcon />,
@ -43,7 +43,7 @@ export default function Urls() {
const copyURL = u => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${u.url}`);
notif.showNotification({
showNotification({
title: 'Copied to clipboard',
message: '',
icon: <CopyIcon />,
@ -86,14 +86,14 @@ export default function Urls() {
const json = await res.json();
if (json.error) {
notif.showNotification({
showNotification({
title: 'Failed to create URL',
message: json.error,
color: 'red',
icon: <CrossIcon />,
});
} else {
notif.showNotification({
showNotification({
title: 'URL shortened',
message: json.url,
color: 'green',
@ -128,7 +128,7 @@ export default function Urls() {
<Group mb='md'>
<Title>URLs</Title>
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}><PlusIcon/></ActionIcon>
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}><PlusIcon /></ActionIcon>
</Group>
<SimpleGrid
@ -145,7 +145,7 @@ export default function Urls() {
<Title>{url.vanity ?? url.id}</Title>
</Group>
<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)}>
<CopyIcon />
</ActionIcon>

View file

@ -1,8 +1,9 @@
import { ActionIcon, Avatar, Button, Card, Group, Modal, SimpleGrid, Skeleton, Switch, TextInput, Title } from '@mantine/core';
import { useForm } from '@mantine/hooks';
import { ActionIcon, Avatar, Button, Card, Group, Modal, SimpleGrid, Skeleton, Stack, Switch, Text, TextInput, Title } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useModals } from '@mantine/modals';
import { useNotifications } from '@mantine/notifications';
import { showNotification } from '@mantine/notifications';
import { CrossIcon, DeleteIcon, PlusIcon } from 'components/icons';
import MutedText from 'components/MutedText';
import useFetch from 'hooks/useFetch';
import { useStoreSelector } from 'lib/redux/store';
import { useRouter } from 'next/router';
@ -17,7 +18,6 @@ function CreateUserModal({ open, setOpen, updateUsers }) {
administrator: false,
},
});
const notif = useNotifications();
const onSubmit = async values => {
const cleanUsername = values.username.trim();
@ -34,14 +34,14 @@ function CreateUserModal({ open, setOpen, updateUsers }) {
setOpen(false);
const res = await useFetch('/api/auth/create', 'POST', data);
if (res.error) {
notif.showNotification({
showNotification({
title: 'Failed to create user',
message: res.error,
icon: <DeleteIcon />,
color: 'red',
});
} else {
notif.showNotification({
showNotification({
title: 'Created user: ' + cleanUsername,
message: '',
icon: <PlusIcon />,
@ -75,7 +75,6 @@ function CreateUserModal({ open, setOpen, updateUsers }) {
export default function Users() {
const user = useStoreSelector(state => state.user);
const router = useRouter();
const notif = useNotifications();
const modals = useModals();
const [users, setUsers] = useState([]);
@ -87,14 +86,14 @@ export default function Users() {
delete_images,
});
if (res.error) {
notif.showNotification({
showNotification({
title: 'Failed to delete user',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
} else {
notif.showNotification({
showNotification({
title: 'User deleted',
message: '',
color: 'green',
@ -108,8 +107,6 @@ export default function Users() {
const openDeleteModal = user => modals.openConfirmModal({
title: `Delete ${user.username}?`,
closeOnConfirm: false,
centered: true,
overlayBlur: 3,
labels: { confirm: 'Yes', cancel: 'No' },
onConfirm: () => {
modals.openConfirmModal({
@ -160,8 +157,12 @@ export default function Users() {
<Card key={user.id} sx={{ maxWidth: '100%' }}>
<Group position='apart'>
<Group position='left'>
<Avatar color={user.administrator ? 'primary' : 'dark'}>{user.username[0]}</Avatar>
<Title>{user.username}</Title>
<Avatar size='lg' color={user.administrator ? 'primary' : 'dark'}>{user.username[0]}</Avatar>
<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 position='right'>
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(user)}>

View file

@ -176,7 +176,6 @@ export class Swift extends Datasource {
public constructor(public config: ConfigSwiftDatasource) {
super();
console.log(config);
this.container = new SwiftContainer({
auth_endpoint_url: config.auth_endpoint,
credentials: {

View file

@ -1,5 +1,5 @@
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 { useRouter } from 'next/router';
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 useFetch from 'hooks/useFetch';
import PasswordStrength from 'components/PasswordStrength';
import { useNotifications } from '@mantine/notifications';
import { showNotification } from '@mantine/notifications';
import { CrossIcon, UserIcon } from 'components/icons';
import { useStoreDispatch } from 'lib/redux/store';
import { updateUser } from 'lib/redux/reducers/user';
@ -20,7 +20,6 @@ export default function Invite({ code, title }) {
const [verifyPasswordError, setVerifyPasswordError] = useState('');
const [strength, setStrength] = useState(0);
const notif = useNotifications();
const dispatch = useStoreDispatch();
const router = useRouter();
@ -53,14 +52,14 @@ export default function Invite({ code, title }) {
const createUser = async () => {
const res = await useFetch('/api/auth/create', 'POST', { code, username, password });
if (res.error) {
notif.showNotification({
showNotification({
title: 'Error while creating user',
message: res.error,
color: 'red',
icon: <CrossIcon />,
});
} else {
notif.showNotification({
showNotification({
title: 'User created',
message: 'You will be logged in shortly...',
color: 'green',

View file

@ -13,10 +13,11 @@ import { version } from '../../package.json';
import type { Config } from 'lib/config/Config';
import type { Datasource } from 'lib/datasources';
const dev = process.env.NODE_ENV === 'development';
let config: Config, datasource: Datasource;
const logger = Logger.get('server');
logger.info(`starting zipline@${version} server`);
logger.info(`starting ${process.env.NODE_ENV || 'production'} zipline@${version} server`);
start();
@ -40,8 +41,6 @@ async function start() {
process.exit(1);
};
const dev = process.env.NODE_ENV === 'development';
process.env.DATABASE_URL = config.core.database_url;
await migrations();

1614
yarn.lock

File diff suppressed because it is too large Load diff