feat: a bunch of changes
This commit is contained in:
parent
20c1d3ef08
commit
a999abfbf8
54 changed files with 1312 additions and 1638 deletions
|
@ -17,7 +17,7 @@
|
|||
"react/no-direct-mutation-state": "warn",
|
||||
"react/no-is-mounted": "warn",
|
||||
"react/no-typos": "error",
|
||||
"react/react-in-jsx-scope": "error",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react/require-render-return": "error",
|
||||
"react/style-prop-object": "warn",
|
||||
"@next/next/no-img-element": "off"
|
||||
|
|
|
@ -57,7 +57,7 @@ yarn start
|
|||
```
|
||||
|
||||
# NGINX Proxy
|
||||
This section requires [nginx](https://nginx.org/).
|
||||
This section requires [NGINX](https://nginx.org/).
|
||||
|
||||
```nginx
|
||||
server {
|
||||
|
@ -97,7 +97,7 @@ curl -H "Content-Type: multipart/form-data" -H "authorization: $TOKEN" -F file=@
|
|||
# Contributing
|
||||
|
||||
## Bug reports
|
||||
Create an issue on GitHub, please include the following:
|
||||
Create an issue on GitHub, please include the following (if one of them is not applicable to the issue then it's not needed):
|
||||
* The steps to reproduce the bug
|
||||
* Logs of Zipline
|
||||
* The version of Zipline
|
||||
|
|
|
@ -1,10 +1,32 @@
|
|||
[core]
|
||||
secure = true
|
||||
secret = 'changethis'
|
||||
secure = true # whether to return https or http in links
|
||||
secret = 'changethis' # change this or zipline will not work
|
||||
host = '0.0.0.0'
|
||||
port = 3000
|
||||
database_url = 'postgres://postgres:postgres@postgres/postgres'
|
||||
|
||||
[datasource]
|
||||
type = 'local' # s3, local, or swift
|
||||
|
||||
[datasource.local]
|
||||
directory = './uploads' # directory to store uploads in
|
||||
|
||||
[datasource.s3]
|
||||
access_key_id = 'AKIAEXAMPLEKEY'
|
||||
secret_access_key = 'somethingsomethingsomething'
|
||||
bucket = 'zipline-storage'
|
||||
endpoint = 's3.amazonaws.com'
|
||||
region = 'us-west-2' # not required, defaults to us-east-1 if not specified
|
||||
force_s3_path = false
|
||||
|
||||
[datasource.swift]
|
||||
container = 'default'
|
||||
auth_endpoint = 'http://127.0.0.1:49155/v3' # only supports v3 swift endpoints at the moment.
|
||||
username = 'swift'
|
||||
password = 'fingertips'
|
||||
project_id = 'Default'
|
||||
domain_id = 'Default'
|
||||
|
||||
[urls]
|
||||
route = '/go'
|
||||
length = 6
|
||||
|
@ -13,7 +35,10 @@ length = 6
|
|||
route = '/u'
|
||||
embed_route = '/a'
|
||||
length = 6
|
||||
directory = './uploads'
|
||||
user_limit = 104900000 # 100mb
|
||||
admin_limit = 104900000 # 100mb
|
||||
disabled_extensions = ['jpg']
|
||||
disabled_extensions = ['jpg']
|
||||
|
||||
[ratelimit]
|
||||
user = 5 # 5 seconds
|
||||
admin = 0 # 0 seconds, disabled
|
|
@ -20,6 +20,8 @@ const { rm } = require('fs/promises');
|
|||
'src/server/util.ts',
|
||||
'src/lib/logger.ts',
|
||||
'src/lib/config.ts',
|
||||
'src/lib/mimes.ts',
|
||||
'src/lib/exts.ts',
|
||||
'src/lib/config/Config.ts',
|
||||
'src/lib/config/readConfig.ts',
|
||||
'src/lib/config/validateConfig.ts',
|
||||
|
|
|
@ -11,4 +11,6 @@ module.exports = {
|
|||
api: {
|
||||
responseLimit: false,
|
||||
},
|
||||
poweredByHeader: false,
|
||||
reactStrictMode: true,
|
||||
};
|
|
@ -44,7 +44,6 @@
|
|||
"react-redux": "^8.0.2",
|
||||
"react-table": "^7.8.0",
|
||||
"redux": "^4.2.0",
|
||||
"uuid": "^8.3.2",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
const { readdir } = require('fs/promises');
|
||||
const { extname } = require('path');
|
||||
const validateConfig = require('../server/validateConfig');
|
||||
const Logger = require('../src/lib/logger');
|
||||
const readConfig = require('../src/lib/readConfig');
|
||||
const mimes = require('./mimes');
|
||||
const { PrismaClient } = require('@prisma/client');
|
||||
|
||||
(async () => {
|
||||
const config = readConfig();
|
||||
|
||||
await validateConfig(config);
|
||||
|
||||
process.env.DATABASE_URL = config.core.database_url;
|
||||
|
||||
const files = await readdir(process.argv[2]);
|
||||
const data = files.map(x => {
|
||||
const mime = mimes[extname(x)] ?? 'application/octet-stream';
|
||||
|
||||
return {
|
||||
file: x,
|
||||
mimetype: mime,
|
||||
userId: 1,
|
||||
};
|
||||
});
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
Logger.get('migrator').info('starting migrations...');
|
||||
await prisma.image.createMany({
|
||||
data,
|
||||
});
|
||||
Logger.get('migrator').info('finished migrations! It is recomended to move your old uploads folder (' + process.argv[2] + ') to the current one which is ' + config.uploader.directory);
|
||||
process.exit();
|
||||
})();
|
|
@ -1,8 +0,0 @@
|
|||
import React from 'react';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
|
||||
export default function Backdrop({ open }) {
|
||||
return (
|
||||
<LoadingOverlay visible={open} />
|
||||
);
|
||||
}
|
|
@ -1,14 +1,9 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Card as MCard,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { Card as MCard, Title } from '@mantine/core';
|
||||
|
||||
export default function Card(props) {
|
||||
const { name, children, ...other } = props;
|
||||
export default function Card({ name, children, ...other }) {
|
||||
|
||||
return (
|
||||
<MCard padding='md' shadow='sm' {...other}>
|
||||
<MCard p='md' shadow='sm' {...other}>
|
||||
<Title order={2}>{name}</Title>
|
||||
{children}
|
||||
</MCard>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import React, { useState } from 'react';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { Button, Card, Group, Image as MImage, Modal, Title } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import { CopyIcon, Cross1Icon, StarIcon, TrashIcon } from '@modulz/radix-icons';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function Image({ image, updateImages }) {
|
||||
export default function File({ image, updateImages }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [t] = useState(image.mimetype.split('/')[0]);
|
||||
const notif = useNotifications();
|
||||
|
@ -56,7 +56,7 @@ export default function Image({ image, updateImages }) {
|
|||
const Type = (props) => {
|
||||
return {
|
||||
'video': <video controls {...props} />,
|
||||
'image': <MImage {...props} />,
|
||||
'image': <MImage withPlaceholder {...props} />,
|
||||
'audio': <audio controls {...props} />,
|
||||
}[t];
|
||||
};
|
|
@ -1,17 +1,12 @@
|
|||
/* eslint-disable jsx-a11y/alt-text */
|
||||
/* eslint-disable react/jsx-key */
|
||||
/* eslint-disable react/display-name */
|
||||
// Code taken from https://codesandbox.io/s/eojw8 and is modified a bit (the entire src/components/table directory)
|
||||
import React from 'react';
|
||||
import {
|
||||
usePagination,
|
||||
useTable,
|
||||
} from 'react-table';
|
||||
import {
|
||||
ActionIcon,
|
||||
createStyles,
|
||||
Divider,
|
||||
Group,
|
||||
Pagination,
|
||||
Group, Image, Pagination,
|
||||
Select,
|
||||
Table,
|
||||
Text,
|
||||
|
@ -22,6 +17,10 @@ import {
|
|||
EnterIcon,
|
||||
TrashIcon,
|
||||
} from '@modulz/radix-icons';
|
||||
import {
|
||||
usePagination,
|
||||
useTable,
|
||||
} from 'react-table';
|
||||
|
||||
const pageSizeOptions = ['10', '25', '50'];
|
||||
|
||||
|
@ -42,6 +41,26 @@ const useStyles = createStyles((t) => ({
|
|||
sortDirectionIcon: { transition: 'transform 200ms ease' },
|
||||
}));
|
||||
|
||||
export function FilePreview({ url, type }) {
|
||||
const Type = props => {
|
||||
return {
|
||||
'video': <video autoPlay controls {...props} />,
|
||||
'image': <Image {...props} />,
|
||||
'audio': <audio autoPlay controls {...props} />,
|
||||
}[type.split('/')[0]];
|
||||
};
|
||||
|
||||
return (
|
||||
<Type
|
||||
sx={{ maxWidth: '10vw', maxHeight: '100vh' }}
|
||||
style={{ maxWidth: '10vw', maxHeight: '100vh' }}
|
||||
mr='sm'
|
||||
src={url}
|
||||
alt={'Unable to preview file'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ImagesTable({
|
||||
columns,
|
||||
data = [],
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
import React, { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { useRouter } from 'next/router';
|
||||
import { useStoreDispatch } from 'lib/redux/store';
|
||||
import { updateUser } from 'lib/redux/reducers/user';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { CheckIcon, CopyIcon, Cross1Icon, FileIcon, GearIcon, HomeIcon, Link1Icon, ResetIcon, UploadIcon, PinRightIcon, PersonIcon, Pencil1Icon, MixerHorizontalIcon } from '@modulz/radix-icons';
|
||||
import { AppShell, Burger, Divider, Group, Header, MediaQuery, Navbar, Paper, Popover, ScrollArea, Select, Text, ThemeIcon, Title, UnstyledButton, useMantineTheme, Box } from '@mantine/core';
|
||||
import { AppShell, Box, Burger, Divider, Group, Header, MediaQuery, Navbar, Paper, Popover, ScrollArea, Select, Text, ThemeIcon, Title, UnstyledButton, useMantineTheme } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { CheckIcon, CopyIcon, Cross1Icon, FileIcon, GearIcon, HomeIcon, Link1Icon, MixerHorizontalIcon, Pencil1Icon, PersonIcon, PinRightIcon, ResetIcon, UploadIcon } from '@modulz/radix-icons';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { updateUser } from 'lib/redux/reducers/user';
|
||||
import { useStoreDispatch } from 'lib/redux/store';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { friendlyThemeName, themes } from './Theming';
|
||||
|
||||
function MenuItemLink(props) {
|
||||
|
@ -198,7 +197,7 @@ export default function Layout({ children, user }) {
|
|||
{items.map(({ icon, text, link }) => (
|
||||
<Link href={link} key={text} passHref>
|
||||
<UnstyledButton
|
||||
sx={{
|
||||
sx={{
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
padding: theme.spacing.xs,
|
||||
|
@ -295,10 +294,13 @@ export default function Layout({ children, user }) {
|
|||
<Text sx={{
|
||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
|
||||
fontWeight: 500,
|
||||
fontSize: theme.fontSizes.xs,
|
||||
fontSize: theme.fontSizes.sm,
|
||||
padding: `${theme.spacing.xs / 2}px ${theme.spacing.sm}px`,
|
||||
cursor: 'default',
|
||||
}}>User: {user.username}</Text>
|
||||
}}
|
||||
>
|
||||
{user.username}
|
||||
</Text>
|
||||
<MenuItemLink icon={<GearIcon />} href='/dashboard/manage'>Manage Account</MenuItemLink>
|
||||
<MenuItem icon={<CopyIcon />} onClick={() => {setOpen(false);openCopyToken();}}>Copy Token</MenuItem>
|
||||
<MenuItem icon={<ResetIcon />} onClick={() => {setOpen(false);openResetToken();}} color='red'>Reset Token</MenuItem>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
/// https://github.com/mui-org/material-ui/blob/next/examples/nextjs/src/Link.js
|
||||
/* eslint-disable jsx-a11y/anchor-has-content */
|
||||
import React, { forwardRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useRouter } from 'next/router';
|
||||
import NextLink from 'next/link';
|
||||
import { Text } from '@mantine/core';
|
||||
import clsx from 'clsx';
|
||||
import NextLink from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
export const NextLinkComposed = forwardRef(function NextLinkComposed(props: any, ref) {
|
||||
const { to, linkAs, href, replace, scroll, passHref, shallow, prefetch, locale, ...other } =
|
||||
|
|
5
src/components/MutedText.tsx
Normal file
5
src/components/MutedText.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { Text } from '@mantine/core';
|
||||
|
||||
export default function MutedText({ children, ...props }) {
|
||||
return <Text color='gray' size='xl' {...props}>{children}</Text>;
|
||||
}
|
29
src/components/SmallTable.tsx
Normal file
29
src/components/SmallTable.tsx
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { Box, Table } from '@mantine/core';
|
||||
import { randomId } from '@mantine/hooks';
|
||||
|
||||
export function SmallTable({ rows, columns }) {
|
||||
return (
|
||||
<Box sx={{ pt: 1 }}>
|
||||
<Table highlightOnHover>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(col => (
|
||||
<th key={randomId()}>{col.name}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(row => (
|
||||
<tr key={randomId()}>
|
||||
{columns.map(col => (
|
||||
<td key={randomId()}>
|
||||
{col.format ? col.format(row[col.id]) : row[col.id]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Text } from '@mantine/core';
|
||||
|
||||
export default function StatText({ children }) {
|
||||
return <Text color='gray' size='xl'>{children}</Text>;
|
||||
}
|
|
@ -1,22 +1,22 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// themes
|
||||
import dark_blue from 'lib/themes/dark_blue';
|
||||
import light_blue from 'lib/themes/light_blue';
|
||||
import dark from 'lib/themes/dark';
|
||||
import ayu_dark from 'lib/themes/ayu_dark';
|
||||
import ayu_mirage from 'lib/themes/ayu_mirage';
|
||||
import ayu_light from 'lib/themes/ayu_light';
|
||||
import nord from 'lib/themes/nord';
|
||||
import ayu_mirage from 'lib/themes/ayu_mirage';
|
||||
import dark from 'lib/themes/dark';
|
||||
import dark_blue from 'lib/themes/dark_blue';
|
||||
import dracula from 'lib/themes/dracula';
|
||||
import light_blue from 'lib/themes/light_blue';
|
||||
import matcha_dark_azul from 'lib/themes/matcha_dark_azul';
|
||||
import nord from 'lib/themes/nord';
|
||||
import qogir_dark from 'lib/themes/qogir_dark';
|
||||
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
import { MantineProvider, MantineThemeOverride } from '@mantine/core';
|
||||
import { useColorScheme } from '@mantine/hooks';
|
||||
import { ModalsProvider } from '@mantine/modals';
|
||||
import { NotificationsProvider } from '@mantine/notifications';
|
||||
import { useColorScheme } from '@mantine/hooks';
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
|
||||
export const themes = {
|
||||
system: (colorScheme: 'dark' | 'light') => colorScheme === 'dark' ? dark_blue : light_blue,
|
||||
|
|
52
src/components/dropzone/Dropzone.tsx
Normal file
52
src/components/dropzone/Dropzone.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import React from 'react';
|
||||
import { Dropzone as MantineDropzone } from '@mantine/dropzone';
|
||||
import { Group, Text, useMantineTheme } from '@mantine/core';
|
||||
import { UploadIcon, CrossCircledIcon, ImageIcon } from '@modulz/radix-icons';
|
||||
|
||||
function ImageUploadIcon({ status, ...props }) {
|
||||
if (status.accepted) {
|
||||
return <UploadIcon {...props} />;
|
||||
}
|
||||
|
||||
if (status.rejected) {
|
||||
return <CrossCircledIcon {...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 }) {
|
||||
const theme = useMantineTheme();
|
||||
|
||||
return (
|
||||
<MantineDropzone loading={loading} onDrop={onDrop}>
|
||||
{status => (
|
||||
<>
|
||||
<Group position='center' spacing='xl' style={{ minHeight: 220, pointerEvents: 'none' }}>
|
||||
<ImageUploadIcon
|
||||
status={status}
|
||||
style={{ width: 80, height: 80, color: getIconColor(status, theme) }}
|
||||
/>
|
||||
|
||||
<Text size='xl' inline>
|
||||
Drag images here or click to select files
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
</MantineDropzone>
|
||||
);
|
||||
}
|
60
src/components/dropzone/DropzoneFile.tsx
Normal file
60
src/components/dropzone/DropzoneFile.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
/* eslint-disable jsx-a11y/alt-text */
|
||||
import React from 'react';
|
||||
import { Image, Table, Tooltip, Badge, useMantineTheme } from '@mantine/core';
|
||||
|
||||
export function FilePreview({ file }: { file: File }) {
|
||||
const Type = props => {
|
||||
return {
|
||||
'video': <video autoPlay controls {...props} />,
|
||||
'image': <Image withPlaceholder {...props} />,
|
||||
'audio': <audio autoPlay controls {...props} />,
|
||||
}[file.type.split('/')[0]];
|
||||
};
|
||||
|
||||
return (
|
||||
<Type
|
||||
sx={{ maxWidth: '10vw', maxHeight: '100vh' }}
|
||||
style={{ maxWidth: '10vw', maxHeight: '100vh' }}
|
||||
src={URL.createObjectURL(file)}
|
||||
alt={file.name}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function FileDropzone({ file }: { file: File }) {
|
||||
const theme = useMantineTheme();
|
||||
|
||||
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'>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{file.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{file.type}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Last Modified</td>
|
||||
<td>{new Date(file.lastModified).toLocaleString()}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Badge size='lg'>
|
||||
{file.name}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
|
@ -1,33 +1,19 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { SimpleGrid, Skeleton, Text, Title } from '@mantine/core';
|
||||
import { randomId, useClipboard } from '@mantine/hooks';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import { CopyIcon, Cross1Icon, TrashIcon } from '@modulz/radix-icons';
|
||||
import Card from 'components/Card';
|
||||
import ZiplineImage from 'components/Image';
|
||||
import File from 'components/File';
|
||||
import ImagesTable from 'components/ImagesTable';
|
||||
import Link from 'components/Link';
|
||||
import MutedText from 'components/MutedText';
|
||||
import { bytesToRead } from 'lib/clientUtils';
|
||||
import useFetch from 'lib/hooks/useFetch';
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
import { Text, Skeleton, Title, SimpleGrid } from '@mantine/core';
|
||||
import { randomId, useClipboard } from '@mantine/hooks';
|
||||
import Link from 'components/Link';
|
||||
import { CopyIcon, Cross1Icon, TrashIcon } from '@modulz/radix-icons';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import StatText from 'components/StatText';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
type Aligns = 'inherit' | 'right' | 'left' | 'center' | 'justify';
|
||||
|
||||
export function bytesToRead(bytes: number) {
|
||||
if (isNaN(bytes)) return '0.0 B';
|
||||
if (bytes === Infinity) return '0.0 B';
|
||||
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
|
||||
let num = 0;
|
||||
|
||||
while (bytes > 1024) {
|
||||
bytes /= 1024;
|
||||
++num;
|
||||
}
|
||||
|
||||
return `${bytes.toFixed(1)} ${units[num]}`;
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const user = useStoreSelector(state => state.user);
|
||||
|
||||
|
@ -86,8 +72,8 @@ export default function Dashboard() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Title>Welcome back {user?.username}</Title>
|
||||
<Text color='gray' sx={{ paddingBottom: 4 }}>You have <b>{images.length ? images.length : '...'}</b> files</Text>
|
||||
<Title>Welcome back, {user?.username}</Title>
|
||||
<Text color='gray' mb='sm'>You have <b>{images.length ? images.length : '...'}</b> files</Text>
|
||||
|
||||
<Title>Recent Files</Title>
|
||||
<SimpleGrid
|
||||
|
@ -98,7 +84,7 @@ export default function Dashboard() {
|
|||
]}
|
||||
>
|
||||
{recent.length ? recent.map(image => (
|
||||
<ZiplineImage key={randomId()} image={image} updateImages={updateImages} />
|
||||
<File key={randomId()} image={image} updateImages={updateImages} />
|
||||
)) : [1,2,3,4].map(x => (
|
||||
<div key={x}>
|
||||
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }}/>
|
||||
|
@ -116,20 +102,22 @@ export default function Dashboard() {
|
|||
]}
|
||||
>
|
||||
<Card name='Size' sx={{ height: '100%' }}>
|
||||
<StatText>{stats ? stats.size : <Skeleton height={8} />}</StatText>
|
||||
<MutedText>{stats ? stats.size : <Skeleton height={8} />}</MutedText>
|
||||
<Title order={2}>Average Size</Title>
|
||||
<StatText>{stats ? bytesToRead(stats.size_num / stats.count) : <Skeleton height={8} />}</StatText>
|
||||
<MutedText>{stats ? bytesToRead(stats.size_num / stats.count) : <Skeleton height={8} />}</MutedText>
|
||||
</Card>
|
||||
<Card name='Images' sx={{ height: '100%' }}>
|
||||
<StatText>{stats ? stats.count : <Skeleton height={8} />}</StatText>
|
||||
<MutedText>{stats ? stats.count : <Skeleton height={8} />}</MutedText>
|
||||
<Title order={2}>Views</Title>
|
||||
<StatText>{stats ? `${stats.views_count} (${isNaN(stats.views_count / stats.count) ? 0 : Math.round(stats.views_count / stats.count)})` : <Skeleton height={8} />}</StatText>
|
||||
<MutedText>{stats ? `${stats.views_count} (${isNaN(stats.views_count / stats.count) ? 0 : Math.round(stats.views_count / stats.count)})` : <Skeleton height={8} />}</MutedText>
|
||||
</Card>
|
||||
<Card name='Users' sx={{ height: '100%' }}>
|
||||
<StatText>{stats ? stats.count_users : <Skeleton height={8} />}</StatText>
|
||||
<MutedText>{stats ? stats.count_users : <Skeleton height={8} />}</MutedText>
|
||||
</Card>
|
||||
</SimpleGrid>
|
||||
|
||||
<Title mt='md'>Files</Title>
|
||||
<Text>View your gallery <Link href='/dashboard/files'>here</Link>.</Text>
|
||||
<ImagesTable
|
||||
columns={[
|
||||
{ accessor: 'file', Header: 'Name', minWidth: 170, align: 'inherit' as Aligns },
|
||||
|
@ -141,34 +129,6 @@ export default function Dashboard() {
|
|||
copyImage={copyImage}
|
||||
viewImage={viewImage}
|
||||
/>
|
||||
|
||||
{/* <Title mt='md'>Files</Title>
|
||||
<Text>View previews of your files in the <Link href='/dashboard/files'>browser</Link>.</Text>
|
||||
<ReactTable
|
||||
columns={[
|
||||
{ accessor: 'file', Header: 'Name', minWidth: 170, align: 'inherit' as Aligns },
|
||||
{ accessor: 'mimetype', Header: 'Type', minWidth: 100, align: 'inherit' as Aligns },
|
||||
{ accessor: 'created_at', Header: 'Date' },
|
||||
]}
|
||||
data={images}
|
||||
pagination
|
||||
/>
|
||||
<Card name='Files per User' mt={22}>
|
||||
<StatTable
|
||||
columns={[
|
||||
{ id: 'username', name: 'Name' },
|
||||
{ id: 'count', name: 'Files' },
|
||||
]}
|
||||
rows={stats ? stats.count_by_user : []} />
|
||||
</Card>
|
||||
<Card name='Types' mt={22}>
|
||||
<StatTable
|
||||
columns={[
|
||||
{ id: 'mimetype', name: 'Type' },
|
||||
{ id: 'count', name: 'Count' },
|
||||
]}
|
||||
rows={stats ? stats.types_count : []} />
|
||||
</Card> */}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import ZiplineImage from 'components/Image';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { Box, Accordion, Pagination, Title, SimpleGrid, Skeleton, Group, ActionIcon } from '@mantine/core';
|
||||
import { Accordion, ActionIcon, Box, Group, Pagination, SimpleGrid, Skeleton, Title } from '@mantine/core';
|
||||
import { PlusIcon } from '@modulz/radix-icons';
|
||||
import File from 'components/File';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function Files() {
|
||||
const [pages, setPages] = useState([]);
|
||||
|
@ -27,48 +26,50 @@ export default function Files() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Group>
|
||||
<Title sx={{ marginBottom: 12 }}>Files</Title>
|
||||
<Group mb='md'>
|
||||
<Title>Files</Title>
|
||||
<Link href='/dashboard/upload' passHref>
|
||||
<ActionIcon component='a' variant='filled' color='primary'><PlusIcon/></ActionIcon>
|
||||
</Link>
|
||||
</Group>
|
||||
<Accordion
|
||||
offsetIcon={false}
|
||||
sx={t => ({
|
||||
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>}>
|
||||
<SimpleGrid
|
||||
cols={3}
|
||||
spacing='lg'
|
||||
breakpoints={[
|
||||
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
|
||||
]}
|
||||
>
|
||||
{favoritePages.length ? favoritePages[(favoritePage - 1) ?? 0].map(image => (
|
||||
<div key={image.id}>
|
||||
<ZiplineImage 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>
|
||||
</Accordion>
|
||||
{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] ,
|
||||
})}
|
||||
>
|
||||
<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>
|
||||
</Accordion>
|
||||
) : null}
|
||||
<SimpleGrid
|
||||
cols={3}
|
||||
spacing='lg'
|
||||
|
@ -78,7 +79,7 @@ export default function Files() {
|
|||
>
|
||||
{pages.length ? pages[(page - 1) ?? 0].map(image => (
|
||||
<div key={image.id}>
|
||||
<ZiplineImage image={image} updateImages={() => updatePages(true)} />
|
||||
<File image={image} updateImages={() => updatePages(true)} />
|
||||
</div>
|
||||
)) : [1,2,3,4].map(x => (
|
||||
<div key={x}>
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import Link from 'components/Link';
|
||||
import { useStoreDispatch, useStoreSelector } from 'lib/redux/store';
|
||||
import { updateUser } from 'lib/redux/reducers/user';
|
||||
import { Box, Button, Card, ColorInput, Group, MultiSelect, Space, Text, TextInput, PasswordInput, Title, Tooltip } from '@mantine/core';
|
||||
import { randomId, useForm, useInterval } from '@mantine/hooks';
|
||||
import { Card, Tooltip, TextInput, Button, Text, Title, Group, ColorInput, MultiSelect, Space, Box, Table } from '@mantine/core';
|
||||
import { DownloadIcon, Cross1Icon, TrashIcon } from '@modulz/radix-icons';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import { Cross1Icon, DownloadIcon, TrashIcon } from '@modulz/radix-icons';
|
||||
import Link from 'components/Link';
|
||||
import { SmallTable } from 'components/SmallTable';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
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 (
|
||||
|
@ -18,7 +19,7 @@ function VarsTooltip({ children }) {
|
|||
<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://zipline.diced.cf/docs/variables'>the docs</Link> for more variables
|
||||
visit <Link href='https://zipl.vercel.app/docs/variables'>the docs</Link> for more variables
|
||||
</>
|
||||
}>
|
||||
{children}
|
||||
|
@ -30,33 +31,6 @@ 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>;
|
||||
}
|
||||
|
||||
function ExportTable({ rows, columns }) {
|
||||
return (
|
||||
<Box sx={{ pt: 1 }} >
|
||||
<Table highlightOnHover>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(col => (
|
||||
<th key={randomId()}>{col.name}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(row => (
|
||||
<tr key={randomId()}>
|
||||
{columns.map(col => (
|
||||
<td key={randomId()}>
|
||||
{col.format ? col.format(row[col.id]) : row[col.id]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Manage() {
|
||||
const user = useStoreSelector(state => state.user);
|
||||
const dispatch = useStoreDispatch();
|
||||
|
@ -176,8 +150,9 @@ export default function Manage() {
|
|||
const res = await useFetch('/api/user/export');
|
||||
|
||||
setExports(res.exports.map(s => ({
|
||||
date: new Date(Number(s.split('_')[3].slice(0, -4))),
|
||||
full: s,
|
||||
date: new Date(Number(s.name.split('_')[3].slice(0, -4))),
|
||||
size: s.size,
|
||||
full: s.name,
|
||||
})).sort((a, b) => a.date.getTime() - b.date.getTime()));
|
||||
};
|
||||
|
||||
|
@ -226,7 +201,6 @@ export default function Manage() {
|
|||
},
|
||||
});
|
||||
|
||||
|
||||
const interval = useInterval(() => getExports(), 30000);
|
||||
useEffect(() => {
|
||||
getExports();
|
||||
|
@ -241,7 +215,7 @@ export default function Manage() {
|
|||
</VarsTooltip>
|
||||
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
|
||||
<TextInput id='username' label='Username' {...form.getInputProps('username')} />
|
||||
<TextInput id='password' label='Password' type='password' {...form.getInputProps('password')} />
|
||||
<PasswordInput id='password' label='Password' description='Leave blank to keep your old password' {...form.getInputProps('password')} />
|
||||
<TextInput id='embedTitle' label='Embed Title' {...form.getInputProps('embedTitle')} />
|
||||
<ColorInput id='embedColor' label='Embed Color' {...form.getInputProps('embedColor')} />
|
||||
<TextInput id='embedSiteName' label='Embed Site Name' {...form.getInputProps('embedSiteName')} />
|
||||
|
@ -258,32 +232,37 @@ export default function Manage() {
|
|||
{...form.getInputProps('domains')}
|
||||
/>
|
||||
|
||||
<Group position='right' sx={{ paddingTop: 12 }}>
|
||||
<Group position='right' mt='md'>
|
||||
<Button
|
||||
type='submit'
|
||||
>Save User</Button>
|
||||
</Group>
|
||||
</form>
|
||||
|
||||
<Title sx={{ paddingTop: 12 }}>Manage Data</Title>
|
||||
<Text color='gray' sx={{ paddingBottom: 12 }}>Delete, or export your data into a zip file.</Text>
|
||||
<Box mb='md'>
|
||||
<Title>Manage Data</Title>
|
||||
<Text color='gray'>Delete, or export your data into a zip file.</Text>
|
||||
</Box>
|
||||
|
||||
<Group>
|
||||
<Button onClick={openDeleteModal} rightIcon={<TrashIcon />}>Delete All Data</Button>
|
||||
<ExportDataTooltip><Button onClick={exportData} rightIcon={<DownloadIcon />}>Export Data</Button></ExportDataTooltip>
|
||||
</Group>
|
||||
<Card mt={22}>
|
||||
<ExportTable
|
||||
<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),
|
||||
})) : []} />
|
||||
</Card>
|
||||
|
||||
<Title sx={{ paddingTop: 12, paddingBottom: 12 }}>ShareX Config</Title>
|
||||
<Title my='md'>ShareX Config</Title>
|
||||
<Group>
|
||||
<Button onClick={() => genShareX(false)} rightIcon={<DownloadIcon />}>ShareX Config</Button>
|
||||
<Button onClick={() => genShareX(true)} rightIcon={<DownloadIcon />}>ShareX Config with Embed</Button>
|
||||
|
|
|
@ -1,52 +1,10 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { SimpleGrid, Skeleton, Title } from '@mantine/core';
|
||||
import Card from 'components/Card';
|
||||
import StatText from 'components/StatText';
|
||||
import MutedText from 'components/MutedText';
|
||||
import { SmallTable } from 'components/SmallTable';
|
||||
import { bytesToRead } from 'lib/clientUtils';
|
||||
import useFetch from 'lib/hooks/useFetch';
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
import { Box, Text, Table, Skeleton, Title, SimpleGrid } from '@mantine/core';
|
||||
import { randomId } from '@mantine/hooks';
|
||||
|
||||
export function bytesToRead(bytes: number) {
|
||||
if (isNaN(bytes)) return '0.0 B';
|
||||
if (bytes === Infinity) return '0.0 B';
|
||||
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
|
||||
let num = 0;
|
||||
|
||||
while (bytes > 1024) {
|
||||
bytes /= 1024;
|
||||
++num;
|
||||
}
|
||||
|
||||
return `${bytes.toFixed(1)} ${units[num]}`;
|
||||
}
|
||||
|
||||
function StatTable({ rows, columns }) {
|
||||
return (
|
||||
<Box sx={{ pt: 1 }}>
|
||||
<Table highlightOnHover>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(col => (
|
||||
<th key={randomId()}>{col.name}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(row => (
|
||||
<tr key={randomId()}>
|
||||
{columns.map(col => (
|
||||
<td key={randomId()}>
|
||||
{col.format ? col.format(row[col.id]) : row[col.id]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function Stats() {
|
||||
const [stats, setStats] = useState(null);
|
||||
|
@ -62,7 +20,7 @@ export default function Stats() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Title>Stats</Title>
|
||||
<Title mb='md'>Stats</Title>
|
||||
<SimpleGrid
|
||||
cols={3}
|
||||
spacing='lg'
|
||||
|
@ -71,22 +29,22 @@ export default function Stats() {
|
|||
]}
|
||||
>
|
||||
<Card name='Size' sx={{ height: '100%' }}>
|
||||
<StatText>{stats ? stats.size : <Skeleton height={8} />}</StatText>
|
||||
<MutedText>{stats ? stats.size : <Skeleton height={8} />}</MutedText>
|
||||
<Title order={2}>Average Size</Title>
|
||||
<StatText>{stats ? bytesToRead(stats.size_num / stats.count) : <Skeleton height={8} />}</StatText>
|
||||
<MutedText>{stats ? bytesToRead(stats.size_num / stats.count) : <Skeleton height={8} />}</MutedText>
|
||||
</Card>
|
||||
<Card name='Images' sx={{ height: '100%' }}>
|
||||
<StatText>{stats ? stats.count : <Skeleton height={8} />}</StatText>
|
||||
<MutedText>{stats ? stats.count : <Skeleton height={8} />}</MutedText>
|
||||
<Title order={2}>Views</Title>
|
||||
<StatText>{stats ? `${stats.views_count} (${isNaN(stats.views_count / stats.count) ? 0 : Math.round(stats.views_count / stats.count)})` : <Skeleton height={8} />}</StatText>
|
||||
<MutedText>{stats ? `${stats.views_count} (${isNaN(stats.views_count / stats.count) ? 0 : Math.round(stats.views_count / stats.count)})` : <Skeleton height={8} />}</MutedText>
|
||||
</Card>
|
||||
<Card name='Users' sx={{ height: '100%' }}>
|
||||
<StatText>{stats ? stats.count_users : <Skeleton height={8} />}</StatText>
|
||||
<MutedText>{stats ? stats.count_users : <Skeleton height={8} />}</MutedText>
|
||||
</Card>
|
||||
</SimpleGrid>
|
||||
|
||||
<Card name='Files per User' mt={22}>
|
||||
<StatTable
|
||||
<SmallTable
|
||||
columns={[
|
||||
{ id: 'username', name: 'Name' },
|
||||
{ id: 'count', name: 'Files' },
|
||||
|
@ -94,7 +52,7 @@ export default function Stats() {
|
|||
rows={stats ? stats.count_by_user : []} />
|
||||
</Card>
|
||||
<Card name='Types' mt={22}>
|
||||
<StatTable
|
||||
<SmallTable
|
||||
columns={[
|
||||
{ id: 'mimetype', name: 'Type' },
|
||||
{ id: 'count', name: 'Count' },
|
||||
|
|
|
@ -1,83 +1,21 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
import Link from 'components/Link';
|
||||
import { Image, Button, Group, Popover, Progress, Text, useMantineTheme, Tooltip, Stack, Table } from '@mantine/core';
|
||||
import { ImageIcon, UploadIcon, CrossCircledIcon } from '@modulz/radix-icons';
|
||||
import { Dropzone } from '@mantine/dropzone';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import { Button, Collapse, Group, Progress, Title, useMantineTheme } from '@mantine/core';
|
||||
import { randomId, useClipboard } from '@mantine/hooks';
|
||||
|
||||
function ImageUploadIcon({ status, ...props }) {
|
||||
if (status.accepted) {
|
||||
return <UploadIcon {...props} />;
|
||||
}
|
||||
|
||||
if (status.rejected) {
|
||||
return <CrossCircledIcon {...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;
|
||||
}
|
||||
|
||||
function ImageDropzone({ file }: { file: File }) {
|
||||
const theme = useMantineTheme();
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
position='top'
|
||||
placement='center'
|
||||
color={theme.colorScheme === 'dark' ? 'dark' : undefined}
|
||||
styles={{
|
||||
body: {
|
||||
color: 'white',
|
||||
},
|
||||
}}
|
||||
label={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Image src={URL.createObjectURL(file)} alt={file.name} sx={{ maxWidth: '10vw', maxHeight: '100vh' }} mr='md' />
|
||||
<Table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>{file.name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Type</td>
|
||||
<td>{file.type}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Last Modified</td>
|
||||
<td>{new Date(file.lastModified).toLocaleString()}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Text weight='bold'>{file.name}</Text>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import { CrossCircledIcon, UploadIcon } from '@modulz/radix-icons';
|
||||
import Dropzone from 'components/dropzone/Dropzone';
|
||||
import FileDropzone from 'components/dropzone/DropzoneFile';
|
||||
import Link from 'components/Link';
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function Upload() {
|
||||
const theme = useMantineTheme();
|
||||
const notif = useNotifications();
|
||||
const clipboard = useClipboard();
|
||||
const user = useStoreSelector(state => state.user);
|
||||
|
||||
const [files, setFiles] = useState([]);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('paste', (e: ClipboardEvent) => {
|
||||
|
@ -93,6 +31,7 @@ export default function Upload() {
|
|||
|
||||
const handleUpload = async () => {
|
||||
setProgress(0);
|
||||
setLoading(true);
|
||||
const body = new FormData();
|
||||
for (let i = 0; i !== files.length; ++i) body.append('file', files[i]);
|
||||
|
||||
|
@ -113,6 +52,7 @@ export default function Upload() {
|
|||
req.addEventListener('load', e => {
|
||||
// @ts-ignore not sure why it thinks response doesnt exist, but it does.
|
||||
const json = JSON.parse(e.target.response);
|
||||
setLoading(false);
|
||||
|
||||
if (json.error === undefined) {
|
||||
notif.updateNotification(id, {
|
||||
|
@ -141,32 +81,17 @@ export default function Upload() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Dropzone onDrop={(f) => setFiles([...files, ...f])}>
|
||||
{status => (
|
||||
<>
|
||||
<Group position='center' spacing='xl' style={{ minHeight: 220, pointerEvents: 'none' }}>
|
||||
<ImageUploadIcon
|
||||
status={status}
|
||||
style={{ width: 80, height: 80, color: getIconColor(status, theme) }}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Text size='xl' inline>
|
||||
Drag images here or click to select files
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
|
||||
|
||||
</>
|
||||
)}
|
||||
<Title mb='md'>Upload Files</Title>
|
||||
|
||||
<Dropzone loading={loading} onDrop={(f) => setFiles([...files, ...f])}>
|
||||
<Group position='center' spacing='md'>
|
||||
{files.map(file => (<FileDropzone key={randomId()} file={file} />))}
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
<Group position='center' spacing='xl' mt={12}>
|
||||
{files.map(file => (<ImageDropzone key={randomId()} file={file} />))}
|
||||
</Group>
|
||||
|
||||
{progress !== 0 && <Progress sx={{ marginTop: 12 }} value={progress} />}
|
||||
<Collapse in={progress !== 0}>
|
||||
{progress !== 0 && <Progress mt='md' value={progress} animate />}
|
||||
</Collapse>
|
||||
|
||||
<Group position='right'>
|
||||
<Button leftIcon={<UploadIcon />} mt={12} onClick={handleUpload}>Upload</Button>
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
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 { CopyIcon, Cross1Icon, Link1Icon, PlusIcon, TrashIcon } from '@modulz/radix-icons';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
import { useClipboard, useForm } from '@mantine/hooks';
|
||||
import { CopyIcon, Cross1Icon, Link1Icon, PlusIcon, TrashIcon } from '@modulz/radix-icons';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import { Modal, Title, Group, Button, Box, Card, TextInput, ActionIcon, SimpleGrid, Skeleton } from '@mantine/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function Urls() {
|
||||
const user = useStoreSelector(state => state.user);
|
||||
|
@ -58,12 +57,18 @@ export default function Urls() {
|
|||
},
|
||||
});
|
||||
|
||||
const onSubmit = async (values) => {
|
||||
const onSubmit = async values => {
|
||||
const cleanURL = values.url.trim();
|
||||
const cleanVanity = values.vanity.trim();
|
||||
|
||||
if (cleanURL === '') return form.setFieldError('url', 'URL can\'t be nothing');
|
||||
|
||||
try {
|
||||
new URL(cleanURL);
|
||||
} catch (e) {
|
||||
return form.setFieldError('url', 'Invalid URL');
|
||||
}
|
||||
|
||||
const data = {
|
||||
url: cleanURL,
|
||||
vanity: cleanVanity === '' ? null : cleanVanity,
|
||||
|
@ -121,8 +126,8 @@ export default function Urls() {
|
|||
</form>
|
||||
</Modal>
|
||||
|
||||
<Group>
|
||||
<Title sx={{ marginBottom: 12 }}>URLs</Title>
|
||||
<Group mb='md'>
|
||||
<Title>URLs</Title>
|
||||
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}><PlusIcon/></ActionIcon>
|
||||
</Group>
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useRouter } from 'next/router';
|
||||
import { ActionIcon, Avatar, Button, Card, Group, Modal, SimpleGrid, Skeleton, Switch, TextInput, Title } from '@mantine/core';
|
||||
import { useForm } from '@mantine/hooks';
|
||||
import { Avatar, Modal, Title, TextInput, Group, Button, Card, ActionIcon, SimpleGrid, Switch, Skeleton, Checkbox } from '@mantine/core';
|
||||
import { Cross1Icon, PlusIcon, TrashIcon } from '@modulz/radix-icons';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import { Cross1Icon, PlusIcon, TrashIcon } from '@modulz/radix-icons';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useStoreSelector } from 'lib/redux/store';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
|
||||
function CreateUserModal({ open, setOpen, updateUsers }) {
|
||||
|
@ -19,7 +19,7 @@ function CreateUserModal({ open, setOpen, updateUsers }) {
|
|||
});
|
||||
const notif = useNotifications();
|
||||
|
||||
const onSubmit = async (values) => {
|
||||
const onSubmit = async values => {
|
||||
const cleanUsername = values.username.trim();
|
||||
const cleanPassword = values.password.trim();
|
||||
if (cleanUsername === '') return form.setFieldError('username', 'Username can\'t be nothing');
|
||||
|
@ -58,7 +58,7 @@ function CreateUserModal({ open, setOpen, updateUsers }) {
|
|||
onClose={() => setOpen(false)}
|
||||
title={<Title>Create User</Title>}
|
||||
>
|
||||
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
|
||||
<form onSubmit={form.onSubmit(v => onSubmit(v))}>
|
||||
<TextInput id='username' label='Username' {...form.getInputProps('username')} />
|
||||
<TextInput id='password' label='Password' type='password' {...form.getInputProps('password')} />
|
||||
<Switch mt={12} id='administrator' label='Administrator' {...form.getInputProps('administrator')} />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Image, User } from '@prisma/client';
|
||||
import type { Image, User } from '@prisma/client';
|
||||
|
||||
export function parse(str: string, image: Image, user: User) {
|
||||
if (!str) return null;
|
||||
|
@ -13,4 +13,18 @@ export function parse(str: string, image: Image, user: User) {
|
|||
.replace(/{image.created_at.full_string}/gi, image.created_at.toLocaleString())
|
||||
.replace(/{image.created_at.time_string}/gi, image.created_at.toLocaleTimeString())
|
||||
.replace(/{image.created_at.date_string}/gi, image.created_at.toLocaleDateString());
|
||||
}
|
||||
|
||||
export function bytesToRead(bytes: number) {
|
||||
if (isNaN(bytes)) return '0.0 B';
|
||||
if (bytes === Infinity) return '0.0 B';
|
||||
const units = ['B', 'kB', 'MB', 'GB', 'TB', 'PB'];
|
||||
let num = 0;
|
||||
|
||||
while (bytes > 1024) {
|
||||
bytes /= 1024;
|
||||
++num;
|
||||
}
|
||||
|
||||
return `${bytes.toFixed(1)} ${units[num]}`;
|
||||
}
|
|
@ -4,7 +4,7 @@ import parse from '@iarna/toml/parse-string';
|
|||
import Logger from '../logger';
|
||||
import { Config } from './Config';
|
||||
|
||||
const e = (val, type, fn) => ({ val, type, fn });
|
||||
const e = (val, type, fn: (c: Config, v: any) => void) => ({ val, type, fn });
|
||||
|
||||
const envValues = [
|
||||
e('SECURE', 'boolean', (c, v) => c.core.secure = v),
|
||||
|
|
|
@ -19,7 +19,7 @@ const validator = object({
|
|||
s3: object({
|
||||
access_key_id: string(),
|
||||
secret_access_key: string(),
|
||||
endpoint: string().notRequired().nullable(),
|
||||
endpoint: string(),
|
||||
bucket: string(),
|
||||
force_s3_path: boolean().default(false),
|
||||
region: string().default('us-east-1'),
|
||||
|
@ -64,6 +64,8 @@ export default function validate(config): Config {
|
|||
errors.push('datasource.s3.secret_access_key is a required field');
|
||||
if (!validated.datasource.s3.bucket)
|
||||
errors.push('datasource.s3.bucket is a required field');
|
||||
if (!validated.datasource.s3.endpoint)
|
||||
errors.push('datasource.s3.endpoint is a required field');
|
||||
if (errors.length) throw { errors };
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -6,15 +6,15 @@ if (!global.datasource) {
|
|||
switch (config.datasource.type) {
|
||||
case 's3':
|
||||
global.datasource = new S3(config.datasource.s3);
|
||||
Logger.get('datasource').info(`Using S3(${config.datasource.s3.bucket}) datasource`);
|
||||
Logger.get('datasource').info(`using S3(${config.datasource.s3.bucket}) datasource`);
|
||||
break;
|
||||
case 'local':
|
||||
global.datasource = new Local(config.datasource.local.directory);
|
||||
Logger.get('datasource').info(`Using local(${config.datasource.local.directory}) datasource`);
|
||||
Logger.get('datasource').info(`using Local(${config.datasource.local.directory}) datasource`);
|
||||
break;
|
||||
case 'swift':
|
||||
global.datasource = new Swift(config.datasource.swift);
|
||||
Logger.get('datasource').info(`Using Swift(${config.datasource.swift.container}) datasource`);
|
||||
Logger.get('datasource').info(`using Swift(${config.datasource.swift.container}) datasource`);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid datasource type');
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// https://github.com/toptal/haste-server/blob/master/static/application.js#L167-L174
|
||||
// Popular extension map
|
||||
module.exports = {
|
||||
const exts = {
|
||||
rb: 'ruby',
|
||||
py: 'python',
|
||||
pl: 'perl',
|
||||
|
@ -35,4 +35,6 @@ module.exports = {
|
|||
txt: '',
|
||||
coffee: 'coffee',
|
||||
swift: 'swift',
|
||||
};
|
||||
};
|
||||
|
||||
export default exts;
|
|
@ -2,7 +2,7 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
|||
import type { CookieSerializeOptions } from 'cookie';
|
||||
|
||||
import { serialize } from 'cookie';
|
||||
import { sign64, unsign64 } from '../util';
|
||||
import { sign64, unsign64 } from 'lib/util';
|
||||
import config from 'lib/config';
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
|
@ -36,7 +36,7 @@ export type NextApiRes = NextApiResponse & {
|
|||
error: (message: string) => void;
|
||||
forbid: (message: string, extra?: any) => void;
|
||||
bad: (message: string) => void;
|
||||
json: (json: any) => void;
|
||||
json: (json: Record<string, any>) => void;
|
||||
ratelimited: (remaining: number) => void;
|
||||
setCookie: (name: string, value: unknown, options: CookieSerializeOptions) => void;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
module.exports = {
|
||||
const mimes = {
|
||||
'.aac': 'audio/aac',
|
||||
'.abw': 'application/x-abiword',
|
||||
'.arc': 'application/x-freearc',
|
||||
|
@ -75,4 +75,6 @@ module.exports = {
|
|||
'.3gp': 'video/3gpp',
|
||||
'.3g2': 'video/3gpp2',
|
||||
'.7z': 'application/x-7z-compressed',
|
||||
};
|
||||
};
|
||||
|
||||
export default mimes;
|
|
@ -2,9 +2,8 @@ import { createHmac, randomBytes, timingSafeEqual } from 'crypto';
|
|||
import { hash, verify } from 'argon2';
|
||||
import { readdir, stat } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import prisma from './prisma';
|
||||
import prisma from 'lib/prisma';
|
||||
import { InvisibleImage, InvisibleUrl } from '@prisma/client';
|
||||
import config from './config';
|
||||
|
||||
export async function hashPassword(s: string): Promise<string> {
|
||||
return await hash(s);
|
||||
|
|
|
@ -1,19 +1,25 @@
|
|||
import React from 'react';
|
||||
import { Box, Text } from '@mantine/core';
|
||||
import { Box, Button, Stack, Text, Title } from '@mantine/core';
|
||||
import Link from 'components/Link';
|
||||
import MutedText from 'components/MutedText';
|
||||
|
||||
export default function FourOhFour() {
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
<Stack
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minHeight: '100vh',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
}}
|
||||
spacing='sm'
|
||||
>
|
||||
<Text size='xl'>404 - Not Found</Text>
|
||||
</Box>
|
||||
<Title sx={{ fontSize: 220, fontWeight: 900, lineHeight: .8 }}>404</Title>
|
||||
<MutedText sx={{ fontSize: 40, fontWeight: 500 }}>This page does not exist!</MutedText>
|
||||
<Button component={Link} href='/dashboard'>Head to the Dashboard</Button>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
25
src/pages/500.tsx
Normal file
25
src/pages/500.tsx
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
import { Button, Stack, Title } from '@mantine/core';
|
||||
import Link from 'components/Link';
|
||||
import MutedText from 'components/MutedText';
|
||||
|
||||
export default function FiveHundred() {
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minHeight: '100vh',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
}}
|
||||
spacing='sm'
|
||||
>
|
||||
<Title sx={{ fontSize: 220, fontWeight: 900, lineHeight: .8 }}>500</Title>
|
||||
<MutedText sx={{ fontSize: 40, fontWeight: 500 }}>Internal Server Error</MutedText>
|
||||
<Button component={Link} href='/dashboard'>Head to the Dashboard</Button>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -5,7 +5,7 @@ import { Box, Button, Modal, PasswordInput } from '@mantine/core';
|
|||
import config from 'lib/config';
|
||||
import prisma from 'lib/prisma';
|
||||
import { parse } from 'lib/clientUtils';
|
||||
import * as exts from '../../scripts/exts';
|
||||
import exts from 'lib/exts';
|
||||
|
||||
export default function EmbeddedImage({ image, user, pass }) {
|
||||
const dataURL = (route: string) => `${route}/${image.file}`;
|
||||
|
|
|
@ -9,7 +9,6 @@ export default function MyApp({ Component, pageProps }) {
|
|||
<Provider store={store}>
|
||||
<Head>
|
||||
<title>{Component.title}</title>
|
||||
<meta name='viewport' content='minimum-scale=1, initial-scale=1, width=device-width' />
|
||||
</Head>
|
||||
<ZiplineTheming Component={Component} pageProps={pageProps} />
|
||||
</Provider>
|
||||
|
|
30
src/pages/_error.tsx
Normal file
30
src/pages/_error.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import React from 'react';
|
||||
import { Button, Stack, Title } from '@mantine/core';
|
||||
import Link from 'components/Link';
|
||||
import MutedText from 'components/MutedText';
|
||||
|
||||
export default function Error({ statusCode }) {
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minHeight: '100vh',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
}}
|
||||
spacing='sm'
|
||||
>
|
||||
<Title sx={{ fontSize: 220, fontWeight: 900, lineHeight: .8 }}>{statusCode}</Title>
|
||||
<MutedText sx={{ fontSize: 40, fontWeight: 500 }}>Something went wrong...</MutedText>
|
||||
<Button component={Link} href='/dashboard'>Head to the Dashboard</Button>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function getInitialProps({ res, err }) {
|
||||
const statusCode = res ? res.statusCode : err ? err.statusCode : 404;
|
||||
return { pageProps: { statusCode } };
|
||||
}
|
|
@ -2,7 +2,7 @@ import prisma from 'lib/prisma';
|
|||
import { NextApiReq, NextApiRes, withZipline } from 'middleware/withZipline';
|
||||
import { checkPassword } from 'lib/util';
|
||||
import datasource from 'lib/datasource';
|
||||
import mimes from '../../../../scripts/mimes';
|
||||
import mimes from 'lib/mimes';
|
||||
import { extname } from 'path';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
|
|
|
@ -6,8 +6,8 @@ import { createInvisImage, randomChars, hashPassword } from 'lib/util';
|
|||
import Logger from 'lib/logger';
|
||||
import { ImageFormat, InvisibleImage } from '@prisma/client';
|
||||
import { format as formatDate } from 'fecha';
|
||||
import { v4 } from 'uuid';
|
||||
import datasource from 'lib/datasource';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
const uploader = multer();
|
||||
|
||||
|
@ -63,7 +63,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
fileName = formatDate(new Date(), 'YYYY-MM-DD_HH:mm:ss');
|
||||
break;
|
||||
case ImageFormat.UUID:
|
||||
fileName = v4();
|
||||
fileName = randomUUID({ disableEntropyCache: true });
|
||||
break;
|
||||
case ImageFormat.NAME:
|
||||
fileName = file.originalname.split('.')[0];
|
||||
|
|
|
@ -3,7 +3,7 @@ import prisma from 'lib/prisma';
|
|||
import Logger from 'lib/logger';
|
||||
import { Zip, ZipPassThrough } from 'fflate';
|
||||
import datasource from 'lib/datasource';
|
||||
import { readdir } from 'fs/promises';
|
||||
import { readdir, stat } from 'fs/promises';
|
||||
import { createReadStream, createWriteStream } from 'fs';
|
||||
|
||||
async function handler(req: NextApiReq, res: NextApiRes) {
|
||||
|
@ -21,6 +21,7 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
const export_name = `zipline_export_${user.id}_${Date.now()}.zip`;
|
||||
const write_stream = createWriteStream(`/tmp/${export_name}`);
|
||||
|
||||
// i found this on some stack overflow thing, forgot the url
|
||||
const onBackpressure = (stream, outputStream, cb) => {
|
||||
const runCb = () => {
|
||||
// Pause if either output or internal backpressure should be applied
|
||||
|
@ -85,7 +86,6 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
}
|
||||
};
|
||||
|
||||
// for (const file of files) {
|
||||
Logger.get('user').info(`Export for ${user.username} (${user.id}) has started`);
|
||||
for (let i = 0; i !== files.length; ++i) {
|
||||
const file = files[i];
|
||||
|
@ -123,7 +123,15 @@ async function handler(req: NextApiReq, res: NextApiRes) {
|
|||
stream.pipe(res);
|
||||
} else {
|
||||
const files = await readdir('/tmp');
|
||||
const exports = files.filter(f => f.startsWith('zipline_export_'));
|
||||
const exp = files.filter(f => f.startsWith('zipline_export_'));
|
||||
const exports = [];
|
||||
for (let i = 0; i !== exp.length; ++i) {
|
||||
const name = exp[i];
|
||||
const stats = await stat(`/tmp/${name}`);
|
||||
|
||||
exports.push({ name, size: stats.size });
|
||||
}
|
||||
|
||||
res.json({
|
||||
exports,
|
||||
});
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { Button, Center, TextInput, Title, PasswordInput } from '@mantine/core';
|
||||
import { useForm } from '@mantine/hooks';
|
||||
import { TextInput, Button, Center, Title, Box, Badge, Tooltip } from '@mantine/core';
|
||||
import { useNotifications } from '@mantine/notifications';
|
||||
import { Cross1Icon, DownloadIcon } from '@modulz/radix-icons';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const notif = useNotifications();
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
|
@ -28,12 +25,12 @@ export default function Login() {
|
|||
});
|
||||
|
||||
if (res.error) {
|
||||
notif.showNotification({
|
||||
title: 'Login Failed',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <Cross1Icon />,
|
||||
});
|
||||
if (res.error.startsWith('403')) {
|
||||
form.setFieldError('password', 'Invalid password');
|
||||
} else {
|
||||
form.setFieldError('username', 'Invalid username');
|
||||
form.setFieldError('password', 'Invalid password');
|
||||
}
|
||||
} else {
|
||||
await router.push(router.query.url as string || '/dashboard');
|
||||
}
|
||||
|
@ -53,7 +50,7 @@ export default function Login() {
|
|||
<Title align='center'>Zipline</Title>
|
||||
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
|
||||
<TextInput size='lg' id='username' label='Username' {...form.getInputProps('username')} />
|
||||
<TextInput size='lg' id='password' label='Password' type='password' {...form.getInputProps('password')} />
|
||||
<PasswordInput size='lg' id='password' label='Password' {...form.getInputProps('password')} />
|
||||
|
||||
<Button size='lg' type='submit' fullWidth mt={12}>Login</Button>
|
||||
</form>
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
import { useStoreDispatch } from 'lib/redux/store';
|
||||
import { updateUser } from 'lib/redux/reducers/user';
|
||||
|
||||
export default function Logout() {
|
||||
const dispatch = useStoreDispatch();
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -10,7 +13,10 @@ export default function Logout() {
|
|||
const userRes = await fetch('/api/user');
|
||||
if (userRes.ok) {
|
||||
const res = await fetch('/api/auth/logout');
|
||||
if (res.ok) router.push('/auth/login');
|
||||
if (res.ok) {
|
||||
dispatch(updateUser(null));
|
||||
router.push('/auth/login');
|
||||
}
|
||||
} else {
|
||||
router.push('/auth/login');
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import exts from '../../../scripts/exts';
|
||||
import exts from 'lib/exts';
|
||||
import { Prism } from '@mantine/prism';
|
||||
|
||||
export default function Code() {
|
||||
|
|
|
@ -2,11 +2,12 @@ import React from 'react';
|
|||
import useLogin from 'hooks/useLogin';
|
||||
import Layout from 'components/Layout';
|
||||
import Files from 'components/pages/Files';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
|
||||
export default function FilesPage() {
|
||||
const { user, loading } = useLogin();
|
||||
|
||||
if (loading) return null;
|
||||
if (loading) return <LoadingOverlay visible={loading} />;
|
||||
|
||||
return (
|
||||
<Layout
|
||||
|
@ -17,4 +18,4 @@ export default function FilesPage() {
|
|||
);
|
||||
}
|
||||
|
||||
FilesPage.title = 'Zipline - Gallery';
|
||||
FilesPage.title = 'Zipline - Files';
|
|
@ -2,10 +2,12 @@ import React from 'react';
|
|||
import useLogin from 'hooks/useLogin';
|
||||
import Layout from 'components/Layout';
|
||||
import Dashboard from 'components/pages/Dashboard';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { user, loading } = useLogin();
|
||||
if (loading) return null;
|
||||
|
||||
if (loading) return <LoadingOverlay visible={loading} />;
|
||||
|
||||
return (
|
||||
<Layout
|
||||
|
|
|
@ -2,11 +2,12 @@ import React from 'react';
|
|||
import useLogin from 'hooks/useLogin';
|
||||
import Layout from 'components/Layout';
|
||||
import Manage from 'components/pages/Manage';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
|
||||
export default function ManagePage() {
|
||||
const { user, loading } = useLogin();
|
||||
|
||||
if (loading) return null;
|
||||
if (loading) return <LoadingOverlay visible={loading} />;
|
||||
|
||||
return (
|
||||
<Layout
|
||||
|
|
|
@ -2,10 +2,12 @@ import React from 'react';
|
|||
import useLogin from 'hooks/useLogin';
|
||||
import Layout from 'components/Layout';
|
||||
import Stats from 'components/pages/Stats';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
|
||||
export default function StatsPage() {
|
||||
const { user, loading } = useLogin();
|
||||
if (loading) return null;
|
||||
|
||||
if (loading) return <LoadingOverlay visible={loading} />;
|
||||
|
||||
return (
|
||||
<Layout
|
||||
|
|
|
@ -2,11 +2,12 @@ import React from 'react';
|
|||
import useLogin from 'hooks/useLogin';
|
||||
import Layout from 'components/Layout';
|
||||
import Upload from 'components/pages/Upload';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
|
||||
export default function UploadPage({ route }) {
|
||||
export default function UploadPage() {
|
||||
const { user, loading } = useLogin();
|
||||
|
||||
if (loading) return null;
|
||||
if (loading) return <LoadingOverlay visible={loading} />;
|
||||
|
||||
return (
|
||||
<Layout
|
||||
|
|
|
@ -2,11 +2,12 @@ import React from 'react';
|
|||
import useLogin from 'hooks/useLogin';
|
||||
import Layout from 'components/Layout';
|
||||
import Urls from 'components/pages/Urls';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
|
||||
export default function UrlsPage() {
|
||||
const { user, loading } = useLogin();
|
||||
|
||||
if (loading) return null;
|
||||
if (loading) return <LoadingOverlay visible={loading} />;
|
||||
|
||||
return (
|
||||
<Layout
|
||||
|
|
|
@ -2,11 +2,12 @@ import React from 'react';
|
|||
import useLogin from 'hooks/useLogin';
|
||||
import Layout from 'components/Layout';
|
||||
import Users from 'components/pages/Users';
|
||||
import { LoadingOverlay } from '@mantine/core';
|
||||
|
||||
export default function UsersPage() {
|
||||
const { user, loading } = useLogin();
|
||||
|
||||
if (loading) return null;
|
||||
if (loading) return <LoadingOverlay visible={loading} />;
|
||||
|
||||
return (
|
||||
<Layout
|
||||
|
|
|
@ -7,11 +7,13 @@ import { extname } from 'path';
|
|||
import { mkdir } from 'fs/promises';
|
||||
import { getStats, log, migrations } from './util';
|
||||
import Logger from '../lib/logger';
|
||||
import mimes from '../../scripts/mimes';
|
||||
import exts from '../../scripts/exts';
|
||||
import mimes from '../lib/mimes';
|
||||
import exts from '../lib/exts';
|
||||
import { version } from '../../package.json';
|
||||
import config from '../lib/config';
|
||||
import datasource from '../lib/datasource';
|
||||
import type { Config } from 'lib/config/Config';
|
||||
import type { Datasource } from 'lib/datasources';
|
||||
|
||||
let config: Config, datasource: Datasource;
|
||||
|
||||
const logger = Logger.get('server');
|
||||
logger.info(`starting zipline@${version} server`);
|
||||
|
@ -19,6 +21,13 @@ logger.info(`starting zipline@${version} server`);
|
|||
start();
|
||||
|
||||
async function start() {
|
||||
const c = await import('../lib/config.js');
|
||||
config = c.default.default;
|
||||
|
||||
const d = await import('../lib/datasource.js');
|
||||
// @ts-ignore
|
||||
datasource = d.default.default;
|
||||
|
||||
// annoy user if they didnt change secret from default "changethis"
|
||||
if (config.core.secret === 'changethis') {
|
||||
logger.error('Secret is not set!');
|
||||
|
@ -64,7 +73,6 @@ async function start() {
|
|||
],
|
||||
},
|
||||
});
|
||||
console.log(image);
|
||||
|
||||
if (!image) await rawFile(req, res, nextServer, params.id);
|
||||
|
||||
|
@ -102,7 +110,7 @@ async function start() {
|
|||
});
|
||||
|
||||
http.on('listening', () => {
|
||||
logger.info(`Listening on ${config.core.host}:${config.core.port}`);
|
||||
logger.info(`listening on ${config.core.host}:${config.core.port}`);
|
||||
});
|
||||
|
||||
http.listen(config.core.port, config.core.host ?? '0.0.0.0');
|
||||
|
|
Loading…
Reference in a new issue