feat: prettier run (#200)

* feat: prettier run

* fix: whatever that was

* chore: format more files

* chore: make format command better
This commit is contained in:
Jonathan 2022-10-19 22:43:01 -04:00 committed by GitHub
parent cb7dacd089
commit af0cd26ea0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
165 changed files with 3635 additions and 10100 deletions

View file

@ -1,36 +1,18 @@
{ {
"extends": [ "extends": ["next", "next/core-web-vitals", "plugin:prettier/recommended"],
"next",
"next/core-web-vitals"
],
"rules": { "rules": {
"indent": [ "linebreak-style": ["error", "unix"],
"error",
2,
{
"SwitchCase": 1
}
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [ "quotes": [
"error", "error",
"single" "single",
], {
"semi": [ "avoidEscape": true
"error", }
"always"
],
"comma-dangle": [
"error",
"always-multiline"
],
"jsx-quotes": [
"error",
"prefer-single"
], ],
"semi": ["error", "always"],
"comma-dangle": ["error", "always-multiline"],
"jsx-quotes": ["error", "prefer-single"],
"indent": "off",
"react/prop-types": "off", "react/prop-types": "off",
"react-hooks/rules-of-hooks": "off", "react-hooks/rules-of-hooks": "off",
"react-hooks/exhaustive-deps": "off", "react-hooks/exhaustive-deps": "off",
@ -48,4 +30,4 @@
"jsx-a11y/alt-text": "off", "jsx-a11y/alt-text": "off",
"react/display-name": "off" "react/display-name": "off"
} }
} }

5
.prettierrc.json Normal file
View file

@ -0,0 +1,5 @@
{
"singleQuote": true,
"jsxSingleQuote": true,
"printWidth": 110
}

View file

@ -2,4 +2,4 @@
"editor.tabSize": 2, "editor.tabSize": 2,
"files.eol": "\n", "files.eol": "\n",
"typescript.tsdk": "node_modules/typescript/lib" "typescript.tsdk": "node_modules/typescript/lib"
} }

View file

@ -2,8 +2,8 @@ nodeLinker: node-modules
plugins: plugins:
- path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs
spec: "@yarnpkg/plugin-interactive-tools" spec: '@yarnpkg/plugin-interactive-tools'
yarnPath: .yarn/releases/yarn-3.2.1.cjs yarnPath: .yarn/releases/yarn-3.2.1.cjs
checksumBehavior: "update" checksumBehavior: 'update'

View file

@ -1,18 +1,23 @@
# Contributing # Contributing
## Bug reports ## Bug reports
Create an issue on GitHub, please include the following (if one of them is not applicable to the issue then it's not needed): 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 steps to reproduce the bug
* The version of Zipline - Logs of Zipline
* Your OS & Browser including server OS - The version of Zipline
* What you were expecting to see - Your OS & Browser including server OS
- What you were expecting to see
## Feature requests ## Feature requests
Create an issue on GitHub, please include the following: Create an issue on GitHub, please include the following:
* Breif explanation of the feature in the title (very breif please)
* How it would work (detailed, but optional) - Breif explanation of the feature in the title (very breif please)
- How it would work (detailed, but optional)
## Pull Requests (contributions to the codebase) ## Pull Requests (contributions to the codebase)
Create a pull request on GitHub. If your PR does not pass the action checks, then please fix the errors. If your PR was submitted before a release, and I have pushed a new release, please make sure to update your PR to reflect any changes, usually this is handled by GitHub. Create a pull request on GitHub. If your PR does not pass the action checks, then please fix the errors. If your PR was submitted before a release, and I have pushed a new release, please make sure to update your PR to reflect any changes, usually this is handled by GitHub.
Please make sure your code also reflects the style of the rest of the code. Please make sure your code also reflects the style of the rest of the code.

View file

@ -1,20 +1,21 @@
<div align="center"> <div align="center">
<img src="https://raw.githubusercontent.com/diced/zipline/trunk/public/zipline_small.png"/> <img src="https://raw.githubusercontent.com/diced/zipline/trunk/public/zipline_small.png"/>
A ShareX/file upload server that is easy to use, packed with features, and with an easy setup! A ShareX/file upload server that is easy to use, packed with features, and with an easy setup!
![Stars](https://img.shields.io/github/stars/diced/zipline?logo=github&style=flat) ![Stars](https://img.shields.io/github/stars/diced/zipline?logo=github&style=flat)
![Version](https://img.shields.io/github/package-json/v/diced/zipline?logo=git&logoColor=white&style=flat) ![Version](https://img.shields.io/github/package-json/v/diced/zipline?logo=git&logoColor=white&style=flat)
![GitHub last commit (branch)](https://img.shields.io/github/last-commit/diced/zipline/trunk?logo=git&logoColor=white&style=flat) ![GitHub last commit (branch)](https://img.shields.io/github/last-commit/diced/zipline/trunk?logo=git&logoColor=white&style=flat)
[![Discord](https://img.shields.io/discord/729771078196527176?color=%23777ed3&label=discord&logo=discord&logoColor=white&style=flat)](https://discord.gg/EAhCRfGxCF) [![Discord](https://img.shields.io/discord/729771078196527176?color=%23777ed3&label=discord&logo=discord&logoColor=white&style=flat)](https://discord.gg/EAhCRfGxCF)
![Build](https://img.shields.io/github/workflow/status/diced/zipline/Build?logo=github&style=flat) ![Build](https://img.shields.io/github/workflow/status/diced/zipline/Build?logo=github&style=flat)
[![Docker Image (trunk)](https://img.shields.io/github/workflow/status/diced/zipline/Push%20Docker%20Images?label=Docker%20%28trunk%29&logo=github&style=flat)](https://github.com/diced/zipline/pkgs/container/zipline/?tag=trunk) [![Docker Image (trunk)](https://img.shields.io/github/workflow/status/diced/zipline/Push%20Docker%20Images?label=Docker%20%28trunk%29&logo=github&style=flat)](https://github.com/diced/zipline/pkgs/container/zipline/?tag=trunk)
[![Docker Image (release)](https://img.shields.io/github/workflow/status/diced/zipline/Push%20Release%20Docker%20Images?label=Docker%20%28release%29&logo=github&style=flat)](https://github.com/diced/zipline/pkgs/container/zipline/?tag=latest) [![Docker Image (release)](https://img.shields.io/github/workflow/status/diced/zipline/Push%20Release%20Docker%20Images?label=Docker%20%28release%29&logo=github&style=flat)](https://github.com/diced/zipline/pkgs/container/zipline/?tag=latest)
</div> </div>
## Features ## Features
- Configurable - Configurable
- Fast - Fast
- Built with Next.js & React - Built with Next.js & React
@ -35,6 +36,7 @@
# Usage # Usage
## Install & run with Docker ## Install & run with Docker
This section requires [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/). This section requires [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/).
```shell ```shell
@ -45,11 +47,14 @@ docker-compose up -d
``` ```
### After installing ### After installing
After installing, please edit the `docker-compose.yml` file and find the line that says `SECRET=changethis` and replace `changethis` with a random string. After installing, please edit the `docker-compose.yml` file and find the line that says `SECRET=changethis` and replace `changethis` with a random string.
Ways you could generate the string could be from a password managers generator, or you could just slam your keyboard and hope for the best. Ways you could generate the string could be from a password managers generator, or you could just slam your keyboard and hope for the best.
## Building & running from source ## Building & running from source
This section requires [nodejs](https://nodejs.org), [yarn](https://yarnpkg.com/) or [npm](https://npmjs.com). This section requires [nodejs](https://nodejs.org), [yarn](https://yarnpkg.com/) or [npm](https://npmjs.com).
```shell ```shell
git clone https://github.com/diced/zipline git clone https://github.com/diced/zipline
cd zipline cd zipline
@ -63,6 +68,7 @@ yarn start
``` ```
# NGINX Proxy # NGINX Proxy
This section requires [NGINX](https://nginx.org/). This section requires [NGINX](https://nginx.org/).
```nginx ```nginx
@ -81,14 +87,17 @@ server {
``` ```
# Website # Website
The default port is `3000`, once you have accessed it you can see a login screen. The default credentials are "administrator" and "password". Once you login please immediately change the details to something more secure. You can do this by clicking on the top right corner where it says "administrator" with a gear icon and clicking Manage Account. The default port is `3000`, once you have accessed it you can see a login screen. The default credentials are "administrator" and "password". Once you login please immediately change the details to something more secure. You can do this by clicking on the top right corner where it says "administrator" with a gear icon and clicking Manage Account.
# ShareX (Windows) # ShareX (Windows)
This section requires [ShareX](https://www.getsharex.com/). This section requires [ShareX](https://www.getsharex.com/).
After navigating to Zipline, click on the top right corner where it says your username and click Manage Account. Scroll down to see "ShareX Config", select the one you would prefer using. After this you can import the .sxcu into sharex. [More information here](https://zipl.vercel.app/docs/guides/uploaders/sharex) After navigating to Zipline, click on the top right corner where it says your username and click Manage Account. Scroll down to see "ShareX Config", select the one you would prefer using. After this you can import the .sxcu into sharex. [More information here](https://zipl.vercel.app/docs/guides/uploaders/sharex)
# Flameshot (Linux) # Flameshot (Linux)
This section requires [Flameshot](https://www.flameshot.org/), [jq](https://stedolan.github.io/jq/), and [xsel](https://github.com/kfish/xsel). This section requires [Flameshot](https://www.flameshot.org/), [jq](https://stedolan.github.io/jq/), and [xsel](https://github.com/kfish/xsel).
You can either use the script below, or generate one directly from Zipline (just like how you can generate a ShareX config). You can either use the script below, or generate one directly from Zipline (just like how you can generate a ShareX config).
@ -104,17 +113,22 @@ curl -H "Content-Type: multipart/form-data" -H "authorization: $TOKEN" -F file=@
# Contributing # Contributing
## Bug reports ## Bug reports
Create an issue on GitHub, please include the following (if one of them is not applicable to the issue then it's not needed): 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 steps to reproduce the bug
* The version of Zipline - Logs of Zipline
* Your OS & Browser including server OS - The version of Zipline
* What you were expecting to see - Your OS & Browser including server OS
- What you were expecting to see
## Feature requests ## Feature requests
Create an issue on GitHub, please include the following: Create an issue on GitHub, please include the following:
* Breif explanation of the feature in the title (very breif please)
* How it would work (detailed, but optional) - Breif explanation of the feature in the title (very breif please)
- How it would work (detailed, but optional)
## Pull Requests (contributions to the codebase) ## Pull Requests (contributions to the codebase)
Create a pull request on GitHub. If your PR does not pass the action checks, then please fix the errors. If your PR was submitted before a release, and I have pushed a new release, please make sure to update your PR to reflect any changes, usually this is handled by GitHub.
Create a pull request on GitHub. If your PR does not pass the action checks, then please fix the errors. If your PR was submitted before a release, and I have pushed a new release, please make sure to update your PR to reflect any changes, usually this is handled by GitHub.

View file

@ -9,4 +9,5 @@
| < 2 | :x: | | < 2 | :x: |
## Reporting a Vulnerability ## Reporting a Vulnerability
Report a Vulnerability by issuing a bug report, with exact details with how the vulnerability happened, what "exploits" can happen, and possible fixes (optional). Vulnerability reports are treated with high priority and will be resolved most of the time quickly. Report a Vulnerability by issuing a bug report, with exact details with how the vulnerability happened, what "exploits" can happen, and possible fixes (optional). Vulnerability reports are treated with high priority and will be resolved most of the time quickly.

View file

@ -36,4 +36,4 @@ services:
- 'postgres' - 'postgres'
volumes: volumes:
pg_data: pg_data:

View file

@ -34,4 +34,4 @@ services:
- 'postgres' - 'postgres'
volumes: volumes:
pg_data: pg_data:

View file

@ -40,4 +40,4 @@ const { rm } = require('fs/promises');
sourcemap: true, sourcemap: true,
minify: false, minify: false,
}); });
})(); })();

9680
mimes.json

File diff suppressed because it is too large Load diff

View file

@ -23,4 +23,4 @@ module.exports = {
}, },
poweredByHeader: false, poweredByHeader: false,
reactStrictMode: true, reactStrictMode: true,
}; };

View file

@ -8,6 +8,7 @@
"build-ci": "cross-env ZIPLINE_DOCKER_BUILD=1 npm-run-all build:schema build:next", "build-ci": "cross-env ZIPLINE_DOCKER_BUILD=1 npm-run-all build:schema build:next",
"build:next": "next build", "build:next": "next build",
"build:schema": "prisma generate --schema=prisma/schema.prisma", "build:schema": "prisma generate --schema=prisma/schema.prisma",
"format": "prettier --write ./src/**/*.{ts,tsx} ./*.{md,js,json,yml}",
"migrate:dev": "prisma migrate dev --create-only", "migrate:dev": "prisma migrate dev --create-only",
"start": "tsx src/server", "start": "tsx src/server",
"lint": "next lint", "lint": "next lint",
@ -69,8 +70,10 @@
"esbuild": "^0.14.44", "esbuild": "^0.14.44",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-config-next": "12.1.6", "eslint-config-next": "12.1.6",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.7.1",
"ts-node": "^10.8.1", "ts-node": "^10.8.1",
"tsx": "^3.8.0", "tsx": "^3.8.0",
"typescript": "^4.7.3" "typescript": "^4.7.3"

View file

@ -1,6 +1,5 @@
import { Card as MCard, Title } from '@mantine/core'; import { Card as MCard, Title } from '@mantine/core';
export default function Card({ name, children, ...other }) { export default function Card({ name, children, ...other }) {
return ( return (
<MCard p='md' shadow='sm' {...other}> <MCard p='md' shadow='sm' {...other}>
@ -8,4 +7,4 @@ export default function Card({ name, children, ...other }) {
{children} {children}
</MCard> </MCard>
); );
} }

View file

@ -11,11 +11,5 @@ const useStyles = createStyles((theme, { size }: { size: MantineSize }) => ({
export default function CodeInput({ ...props }) { export default function CodeInput({ ...props }) {
const { classes } = useStyles({ size: 'md' }, { name: 'CodeInput' }); const { classes } = useStyles({ size: 'md' }, { name: 'CodeInput' });
return ( return <Textarea classNames={{ input: classes.input }} autoComplete='nope' {...props} />;
<Textarea
classNames={{ input: classes.input }}
autoComplete='nope'
{...props}
/>
);
} }

View file

@ -4,7 +4,19 @@ import { showNotification } from '@mantine/notifications';
import { relativeTime } from 'lib/utils/client'; import { relativeTime } from 'lib/utils/client';
import { useFileDelete, useFileFavorite } from 'lib/queries/files'; import { useFileDelete, useFileFavorite } from 'lib/queries/files';
import { useState } from 'react'; import { useState } from 'react';
import { CalendarIcon, ClockIcon, CopyIcon, CrossIcon, DeleteIcon, ExternalLinkIcon, FileIcon, HashIcon, ImageIcon, StarIcon, EyeIcon } from './icons'; import {
CalendarIcon,
ClockIcon,
CopyIcon,
CrossIcon,
DeleteIcon,
ExternalLinkIcon,
FileIcon,
HashIcon,
ImageIcon,
StarIcon,
EyeIcon,
} from './icons';
import MutedText from './MutedText'; import MutedText from './MutedText';
import Type from './Type'; import Type from './Type';
import Link from './Link'; import Link from './Link';
@ -76,35 +88,33 @@ export default function File({ image, updateImages, disableMediaPreview }) {
}; };
const handleFavorite = async () => { const handleFavorite = async () => {
favoriteFile.mutate({ id: image.id, favorite: !image.favorite }, { favoriteFile.mutate(
onSuccess: () => { { id: image.id, favorite: !image.favorite },
showNotification({ {
title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'), onSuccess: () => {
message: '', showNotification({
icon: <StarIcon />, title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'),
}); message: '',
}, icon: <StarIcon />,
});
},
onError: (res: any) => { onError: (res: any) => {
showNotification({ showNotification({
title: 'Failed to favorite file', title: 'Failed to favorite file',
message: res.error, message: res.error,
color: 'red', color: 'red',
icon: <CrossIcon />, icon: <CrossIcon />,
}); });
}, },
}); }
);
}; };
console.log(image); console.log(image);
return ( return (
<> <>
<Modal <Modal opened={open} onClose={() => setOpen(false)} title={<Title>{image.file}</Title>} size='xl'>
opened={open}
onClose={() => setOpen(false)}
title={<Title>{image.file}</Title>}
size='xl'
>
<LoadingOverlay visible={loading} /> <LoadingOverlay visible={loading} />
<Stack> <Stack>
<Type <Type
@ -120,13 +130,19 @@ export default function File({ image, updateImages, disableMediaPreview }) {
<FileMeta Icon={FileIcon} title='Name' subtitle={image.file} /> <FileMeta Icon={FileIcon} title='Name' subtitle={image.file} />
<FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} /> <FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} />
<FileMeta Icon={EyeIcon} title='Views' subtitle={image.views} /> <FileMeta Icon={EyeIcon} title='Views' subtitle={image.views} />
<FileMeta Icon={CalendarIcon} title='Uploaded at' subtitle={new Date(image.created_at).toLocaleString()} /> <FileMeta
{image.expires_at && <FileMeta Icon={CalendarIcon}
Icon={ClockIcon} title='Uploaded at'
title='Expires' subtitle={new Date(image.created_at).toLocaleString()}
subtitle={relativeTime(new Date(image.expires_at))} />
tooltip={new Date(image.expires_at).toLocaleString()} {image.expires_at && (
/>} <FileMeta
Icon={ClockIcon}
title='Expires'
subtitle={relativeTime(new Date(image.expires_at))}
tooltip={new Date(image.expires_at).toLocaleString()}
/>
)}
<FileMeta Icon={HashIcon} title='ID' subtitle={image.id} /> <FileMeta Icon={HashIcon} title='ID' subtitle={image.id} />
</Stack> </Stack>
</Stack> </Stack>
@ -145,8 +161,20 @@ export default function File({ image, updateImages, disableMediaPreview }) {
<LoadingOverlay visible={loading} /> <LoadingOverlay visible={loading} />
<Type <Type
file={image} file={image}
sx={{ minHeight: 200, maxHeight: 320, fontSize: 70, width: '100%', cursor: 'pointer' }} sx={{
style={{ minHeight: 200, maxHeight: 320, fontSize: 70, width: '100%', cursor: 'pointer' }} minHeight: 200,
maxHeight: 320,
fontSize: 70,
width: '100%',
cursor: 'pointer',
}}
style={{
minHeight: 200,
maxHeight: 320,
fontSize: 70,
width: '100%',
cursor: 'pointer',
}}
src={image.url} src={image.url}
alt={image.file} alt={image.file}
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
@ -156,4 +184,4 @@ export default function File({ image, updateImages, disableMediaPreview }) {
</Card> </Card>
</> </>
); );
} }

View file

@ -1,4 +1,27 @@
import { AppShell, Box, Burger, Button, Divider, Header, MediaQuery, Navbar, NavLink, Paper, Popover, ScrollArea, Select, Stack, Text, Title, UnstyledButton, useMantineTheme, Group, Image, Tooltip, Badge } from '@mantine/core'; import {
AppShell,
Box,
Burger,
Button,
Divider,
Header,
MediaQuery,
Navbar,
NavLink,
Paper,
Popover,
ScrollArea,
Select,
Stack,
Text,
Title,
UnstyledButton,
useMantineTheme,
Group,
Image,
Tooltip,
Badge,
} from '@mantine/core';
import { useClipboard } from '@mantine/hooks'; import { useClipboard } from '@mantine/hooks';
import { useModals } from '@mantine/modals'; import { useModals } from '@mantine/modals';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
@ -9,7 +32,24 @@ import { useRecoilState } from 'recoil';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useState } from 'react'; import { useState } from 'react';
import { ExternalLinkIcon, ActivityIcon, CheckIcon, CopyIcon, CrossIcon, DeleteIcon, FileIcon, HomeIcon, LinkIcon, LogoutIcon, PencilIcon, SettingsIcon, TagIcon, TypeIcon, UploadIcon, UserIcon } from './icons'; import {
ExternalLinkIcon,
ActivityIcon,
CheckIcon,
CopyIcon,
CrossIcon,
DeleteIcon,
FileIcon,
HomeIcon,
LinkIcon,
LogoutIcon,
PencilIcon,
SettingsIcon,
TagIcon,
TypeIcon,
UploadIcon,
UserIcon,
} from './icons';
import { friendlyThemeName, themes } from './Theming'; import { friendlyThemeName, themes } from './Theming';
function MenuItemLink(props) { function MenuItemLink(props) {
@ -23,7 +63,7 @@ function MenuItemLink(props) {
function MenuItem(props) { function MenuItem(props) {
return ( return (
<UnstyledButton <UnstyledButton
sx={theme => ({ sx={(theme) => ({
display: 'block', display: 'block',
width: '100%', width: '100%',
padding: 5, padding: 5,
@ -31,30 +71,32 @@ function MenuItem(props) {
color: props.color color: props.color
? theme.fn.themeColor(props.color, theme.colorScheme === 'dark' ? 5 : 7) ? theme.fn.themeColor(props.color, theme.colorScheme === 'dark' ? 5 : 7)
: theme.colorScheme === 'dark' : theme.colorScheme === 'dark'
? theme.colors.dark[0] ? theme.colors.dark[0]
: theme.black, : theme.black,
'&:hover': { '&:hover': {
backgroundColor: props.color backgroundColor: props.color
? theme.fn.rgba( ? theme.fn.rgba(
theme.fn.themeColor(props.color, theme.colorScheme === 'dark' ? 9 : 0), theme.fn.themeColor(props.color, theme.colorScheme === 'dark' ? 9 : 0),
theme.colorScheme === 'dark' ? 0.2 : 1 theme.colorScheme === 'dark' ? 0.2 : 1
) )
: theme.colorScheme === 'dark' : theme.colorScheme === 'dark'
? theme.fn.rgba(theme.colors.dark[3], 0.35) ? theme.fn.rgba(theme.colors.dark[3], 0.35)
: theme.colors.gray[0], : theme.colors.gray[0],
}, },
})} })}
{...props} {...props}
> >
<Group noWrap> <Group noWrap>
<Box sx={theme => ({ <Box
marginRight: theme.spacing.xs / 4, sx={(theme) => ({
paddingLeft: theme.spacing.xs / 2, marginRight: theme.spacing.xs / 4,
paddingLeft: theme.spacing.xs / 2,
'& *': { '& *': {
display: 'block', display: 'block',
}, },
})}> })}
>
{props.icon} {props.icon}
</Box> </Box>
<Text size='sm'>{props.children}</Text> <Text size='sm'>{props.children}</Text>
@ -101,13 +143,13 @@ const admin_items = [
icon: <UserIcon size={18} />, icon: <UserIcon size={18} />,
text: 'Users', text: 'Users',
link: '/dashboard/users', link: '/dashboard/users',
if: props => true, if: (props) => true,
}, },
{ {
icon: <TagIcon size={18} />, icon: <TagIcon size={18} />,
text: 'Invites', text: 'Invites',
link: '/dashboard/invites', link: '/dashboard/invites',
if: props => props.invites, if: (props) => props.invites,
}, },
]; ];
@ -129,7 +171,7 @@ export default function Layout({ children, props }) {
const modals = useModals(); const modals = useModals();
const clipboard = useClipboard(); const clipboard = useClipboard();
const handleUpdateTheme = async value => { const handleUpdateTheme = async (value) => {
const newUser = await useFetch('/api/user', 'PATCH', { const newUser = await useFetch('/api/user', 'PATCH', {
systemTheme: value || 'dark_blue', systemTheme: value || 'dark_blue',
}); });
@ -146,74 +188,70 @@ export default function Layout({ children, props }) {
}); });
}; };
const openResetToken = () => modals.openConfirmModal({ const openResetToken = () =>
title: 'Reset Token', modals.openConfirmModal({
children: ( title: 'Reset Token',
<Text size='sm'> children: (
Once you reset your token, you will have to update any uploaders to use this new token. <Text size='sm'>
</Text> Once you reset your token, you will have to update any uploaders to use this new token.
), </Text>
labels: { confirm: 'Reset', cancel: 'Cancel' }, ),
onConfirm: async () => { labels: { confirm: 'Reset', cancel: 'Cancel' },
const a = await useFetch('/api/user/token', 'PATCH'); onConfirm: async () => {
if (!a.success) { const a = await useFetch('/api/user/token', 'PATCH');
setToken(a.success); if (!a.success) {
setToken(a.success);
showNotification({
title: 'Token Reset Failed',
message: a.error,
color: 'red',
icon: <CrossIcon />,
});
} else {
showNotification({
title: 'Token Reset',
message:
'Your token has been reset. You will need to update any uploaders to use this new token.',
color: 'green',
icon: <CheckIcon />,
});
}
modals.closeAll();
},
});
const openCopyToken = () =>
modals.openConfirmModal({
title: 'Copy Token',
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.
</Text>
),
labels: { confirm: 'Copy', cancel: 'Cancel' },
onConfirm: async () => {
clipboard.copy(token);
showNotification({ showNotification({
title: 'Token Reset Failed', title: 'Token Copied',
message: a.error, message: 'Your token has been copied to your clipboard.',
color: 'red',
icon: <CrossIcon />,
});
} else {
showNotification({
title: 'Token Reset',
message: 'Your token has been reset. You will need to update any uploaders to use this new token.',
color: 'green', color: 'green',
icon: <CheckIcon />, icon: <CheckIcon />,
}); });
}
modals.closeAll(); modals.closeAll();
}, },
}); });
const openCopyToken = () => modals.openConfirmModal({
title: 'Copy Token',
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.
</Text>
),
labels: { confirm: 'Copy', cancel: 'Cancel' },
onConfirm: async () => {
clipboard.copy(token);
showNotification({
title: 'Token Copied',
message: 'Your token has been copied to your clipboard.',
color: 'green',
icon: <CheckIcon />,
});
modals.closeAll();
},
});
return ( return (
<AppShell <AppShell
navbarOffsetBreakpoint='sm' navbarOffsetBreakpoint='sm'
fixed fixed
navbar={ navbar={
<Navbar <Navbar pt='sm' hiddenBreakpoint='sm' hidden={!opened} width={{ sm: 200, lg: 230 }}>
pt='sm' <Navbar.Section grow component={ScrollArea}>
hiddenBreakpoint='sm'
hidden={!opened}
width={{ sm: 200, lg: 230 }}
>
<Navbar.Section
grow
component={ScrollArea}
>
{items.map(({ icon, text, link }) => ( {items.map(({ icon, text, link }) => (
<Link href={link} key={text} passHref> <Link href={link} key={text} passHref>
<NavLink <NavLink
@ -230,34 +268,38 @@ export default function Layout({ children, props }) {
label='Administration' label='Administration'
icon={<SettingsIcon />} icon={<SettingsIcon />}
childrenOffset={28} childrenOffset={28}
defaultOpened={admin_items.map(x => x.link).includes(router.pathname)} defaultOpened={admin_items.map((x) => x.link).includes(router.pathname)}
> >
{admin_items.filter(x => x.if(props)).map(({ icon, text, link }) => ( {admin_items
<Link href={link} key={text} passHref> .filter((x) => x.if(props))
<NavLink .map(({ icon, text, link }) => (
component='a' <Link href={link} key={text} passHref>
label={text} <NavLink
icon={icon} component='a'
active={router.pathname === link} label={text}
variant='light' icon={icon}
/> active={router.pathname === link}
</Link> variant='light'
))} />
</Link>
))}
</NavLink> </NavLink>
)} )}
</Navbar.Section> </Navbar.Section>
<Navbar.Section> <Navbar.Section>
{external_links.length ? external_links.map(({ label, link }, i) => ( {external_links.length
<Link href={link} passHref key={i}> ? external_links.map(({ label, link }, i) => (
<NavLink <Link href={link} passHref key={i}>
label={label} <NavLink
component='a' label={label}
target='_blank' component='a'
variant='light' target='_blank'
icon={<ExternalLinkIcon />} variant='light'
/> icon={<ExternalLinkIcon />}
</Link> />
)) : null} </Link>
))
: null}
</Navbar.Section> </Navbar.Section>
{version.isSuccess ? ( {version.isSuccess ? (
<Navbar.Section> <Navbar.Section>
@ -295,16 +337,12 @@ export default function Layout({ children, props }) {
</MediaQuery> </MediaQuery>
<Title ml='sm'>{title}</Title> <Title ml='sm'>{title}</Title>
<Box sx={{ marginLeft: 'auto', marginRight: 0 }}> <Box sx={{ marginLeft: 'auto', marginRight: 0 }}>
<Popover <Popover position='bottom-end' opened={open} onClose={() => setOpen(false)}>
position='bottom-end'
opened={open}
onClose={() => setOpen(false)}
>
<Popover.Target> <Popover.Target>
<Button <Button
leftIcon={avatar ? <Image src={avatar} height={32} radius='md' /> : <SettingsIcon />} leftIcon={avatar ? <Image src={avatar} height={32} radius='md' /> : <SettingsIcon />}
onClick={() => setOpen((o) => !o)} onClick={() => setOpen((o) => !o)}
sx={t => ({ sx={(t) => ({
backgroundColor: 'inherit', backgroundColor: 'inherit',
'&:hover': { '&:hover': {
backgroundColor: t.other.hover, backgroundColor: t.other.hover,
@ -320,33 +358,59 @@ export default function Layout({ children, props }) {
<Popover.Dropdown p={4} mr='md' sx={{ minWidth: '200px' }}> <Popover.Dropdown p={4} mr='md' sx={{ minWidth: '200px' }}>
<Stack spacing={2}> <Stack spacing={2}>
<Text sx={{ <Text
color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6], sx={{
fontWeight: 500, color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
fontSize: theme.fontSizes.sm, fontWeight: 500,
padding: `${theme.spacing.xs / 2}px ${theme.spacing.sm}px`, fontSize: theme.fontSizes.sm,
cursor: 'default', padding: `${theme.spacing.xs / 2}px ${theme.spacing.sm}px`,
}} cursor: 'default',
}}
> >
{user.username} {user.username}
</Text> </Text>
<MenuItemLink icon={<SettingsIcon />} href='/dashboard/manage'>Manage Account</MenuItemLink> <MenuItemLink icon={<SettingsIcon />} href='/dashboard/manage'>
<MenuItem icon={<CopyIcon />} onClick={() => { setOpen(false); openCopyToken(); }}>Copy Token</MenuItem> Manage Account
<MenuItem icon={<DeleteIcon />} onClick={() => { setOpen(false); openResetToken(); }} color='red'>Reset Token</MenuItem> </MenuItemLink>
<MenuItemLink icon={<LogoutIcon />} href='/auth/logout' color='red'>Logout</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 <Divider
variant='solid' variant='solid'
my={theme.spacing.xs / 2} my={theme.spacing.xs / 2}
sx={theme => ({ sx={(theme) => ({
width: '110%', width: '110%',
borderTopColor: theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.colors.gray[2], borderTopColor:
theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.colors.gray[2],
margin: `${theme.spacing.xs / 2}px -4px`, margin: `${theme.spacing.xs / 2}px -4px`,
})} })}
/> />
<MenuItem icon={<PencilIcon />}> <MenuItem icon={<PencilIcon />}>
<Select <Select
size='xs' size='xs'
data={Object.keys(themes).map(t => ({ value: t, label: friendlyThemeName[t] }))} data={Object.keys(themes).map((t) => ({
value: t,
label: friendlyThemeName[t],
}))}
value={systemTheme} value={systemTheme}
onChange={handleUpdateTheme} onChange={handleUpdateTheme}
/> />
@ -363,7 +427,7 @@ export default function Layout({ children, props }) {
withBorder withBorder
p='md' p='md'
shadow='xs' shadow='xs'
sx={t => ({ sx={(t) => ({
borderColor: t.colorScheme === 'dark' ? t.colors.dark[5] : t.colors.dark[0], borderColor: t.colorScheme === 'dark' ? t.colors.dark[5] : t.colors.dark[0],
})} })}
> >
@ -371,4 +435,4 @@ export default function Layout({ children, props }) {
</Paper> </Paper>
</AppShell> </AppShell>
); );
} }

View file

@ -1,3 +1,3 @@
import { NextLink as Link } from '@mantine/next'; import { NextLink as Link } from '@mantine/next';
export default Link; export default Link;

View file

@ -1,5 +1,9 @@
import { Text } from '@mantine/core'; import { Text } from '@mantine/core';
export default function MutedText({ children, ...props }) { export default function MutedText({ children, ...props }) {
return <Text color='dimmed' size='xl' {...props}>{children}</Text>; return (
} <Text color='dimmed' size='xl' {...props}>
{children}
</Text>
);
}

View file

@ -6,12 +6,7 @@ import { CheckIcon, CrossIcon } from './icons';
function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) { function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) {
return ( return (
<Text <Text color={meets ? 'teal' : 'red'} sx={{ display: 'flex', alignItems: 'center' }} mt='sm' size='sm'>
color={meets ? 'teal' : 'red'}
sx={{ display: 'flex', alignItems: 'center' }}
mt='sm'
size='sm'
>
{meets ? <CheckIcon /> : <CrossIcon />} <Box ml='md'>{label}</Box> {meets ? <CheckIcon /> : <CrossIcon />} <Box ml='md'>{label}</Box>
</Text> </Text>
); );
@ -60,10 +55,7 @@ export default function PasswordStrength({ value, setValue, setStrength, ...prop
}} }}
> >
<Popover.Target> <Popover.Target>
<div <div onFocusCapture={() => setPopoverOpened(true)} onBlurCapture={() => setPopoverOpened(false)}>
onFocusCapture={() => setPopoverOpened(true)}
onBlurCapture={() => setPopoverOpened(false)}
>
<PasswordInput <PasswordInput
label='Password' label='Password'
description='A strong password should include letters in lower and uppercase, at least 1 number, at least 1 special symbol' description='A strong password should include letters in lower and uppercase, at least 1 number, at least 1 special symbol'

View file

@ -7,18 +7,16 @@ export function SmallTable({ rows, columns }) {
<Table highlightOnHover> <Table highlightOnHover>
<thead> <thead>
<tr> <tr>
{columns.map(col => ( {columns.map((col) => (
<th key={randomId()}>{col.name}</th> <th key={randomId()}>{col.name}</th>
))} ))}
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{rows.map(row => ( {rows.map((row) => (
<tr key={randomId()}> <tr key={randomId()}>
{columns.map(col => ( {columns.map((col) => (
<td key={randomId()}> <td key={randomId()}>{col.format ? col.format(row[col.id]) : row[col.id]}</td>
{col.format ? col.format(row[col.id]) : row[col.id]}
</td>
))} ))}
</tr> </tr>
))} ))}
@ -26,4 +24,4 @@ export function SmallTable({ rows, columns }) {
</Table> </Table>
</Box> </Box>
); );
} }

View file

@ -29,9 +29,9 @@ const useStyles = createStyles((theme) => ({
})); }));
interface StatsGridProps { interface StatsGridProps {
stat: { stat: {
title: string; title: string;
icon: React.ReactNode, icon: React.ReactNode;
value: string; value: string;
desc: string; desc: string;
diff?: number; diff?: number;
@ -53,27 +53,14 @@ export default function StatCard({ stat }: StatsGridProps) {
<Group align='flex-end' spacing='xs' mt={25}> <Group align='flex-end' spacing='xs' mt={25}>
<Text className={classes.value}>{stat.value}</Text> <Text className={classes.value}>{stat.value}</Text>
{ {typeof stat.diff == 'number' && (
typeof stat.diff == 'number' && ( <>
<> <Text color={stat.diff >= 0 ? 'teal' : 'red'} size='sm' weight={500} className={classes.diff}>
<Text <span>{stat.diff === Infinity ? '∞' : stat.diff}%</span>
color={stat.diff >= 0 ? 'teal' : 'red'} {stat.diff >= 0 ? <ArrowUpRight size={16} /> : <ArrowDownRight size={16} />}
size='sm' </Text>
weight={500} </>
className={classes.diff} )}
>
<span>{stat.diff === Infinity ? '∞' : stat.diff}%</span>
{
stat.diff >= 0 ? (
<ArrowUpRight size={16} />
) : (
<ArrowDownRight size={16} />
)
}
</Text>
</>
)
}
</Group> </Group>
<Text size='xs' color='dimmed' mt={7}> <Text size='xs' color='dimmed' mt={7}>
@ -81,4 +68,4 @@ export default function StatCard({ stat }: StatsGridProps) {
</Text> </Text>
</Card> </Card>
); );
} }

View file

@ -20,7 +20,7 @@ import { useRecoilValue } from 'recoil';
import { userSelector } from 'lib/recoil/user'; import { userSelector } from 'lib/recoil/user';
export const themes = { export const themes = {
system: (colorScheme: 'dark' | 'light') => colorScheme === 'dark' ? dark_blue : light_blue, system: (colorScheme: 'dark' | 'light') => (colorScheme === 'dark' ? dark_blue : light_blue),
dark_blue, dark_blue,
light_blue, light_blue,
dark, dark,
@ -34,17 +34,17 @@ export const themes = {
}; };
export const friendlyThemeName = { export const friendlyThemeName = {
'system': 'System Theme', system: 'System Theme',
'dark_blue': 'Dark Blue', dark_blue: 'Dark Blue',
'light_blue': 'Light Blue', light_blue: 'Light Blue',
'dark': 'Very Dark', dark: 'Very Dark',
'ayu_dark': 'Ayu Dark', ayu_dark: 'Ayu Dark',
'ayu_mirage': 'Ayu Mirage', ayu_mirage: 'Ayu Mirage',
'ayu_light': 'Ayu Light', ayu_light: 'Ayu Light',
'nord': 'Nord', nord: 'Nord',
'dracula': 'Dracula', dracula: 'Dracula',
'matcha_dark_azul': 'Matcha Dark Azul', matcha_dark_azul: 'Matcha Dark Azul',
'qogir_dark': 'Qogir Dark', qogir_dark: 'Qogir Dark',
}; };
export default function ZiplineTheming({ Component, pageProps, ...props }) { export default function ZiplineTheming({ Component, pageProps, ...props }) {
@ -69,14 +69,14 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
...theme, ...theme,
components: { components: {
AppShell: { AppShell: {
styles: t => ({ styles: (t) => ({
root: { root: {
backgroundColor: t.other.AppShell_backgroundColor, backgroundColor: t.other.AppShell_backgroundColor,
}, },
}), }),
}, },
NavLink: { NavLink: {
styles: t => ({ styles: (t) => ({
icon: { icon: {
paddingLeft: t.spacing.sm, paddingLeft: t.spacing.sm,
}, },
@ -101,14 +101,14 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
}, },
}, },
Card: { Card: {
styles: t => ({ styles: (t) => ({
root: { root: {
backgroundColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0], backgroundColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0],
}, },
}), }),
}, },
Image: { Image: {
styles: t => ({ styles: (t) => ({
placeholder: { placeholder: {
backgroundColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0], backgroundColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0],
}, },
@ -124,4 +124,4 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
</ModalsProvider> </ModalsProvider>
</MantineProvider> </MantineProvider>
); );
} }

View file

@ -5,20 +5,25 @@ import { AudioIcon, FileIcon, PlayIcon } from './icons';
function Placeholder({ text, Icon, ...props }) { function Placeholder({ text, Icon, ...props }) {
if (props.disableResolve) props.src = null; if (props.disableResolve) props.src = null;
return ( return (
<Image height={200} withPlaceholder placeholder={ <Image
<Group> height={200}
<Icon size={48} /> withPlaceholder
<Text size='md'>{text}</Text> placeholder={
</Group> <Group>
} {...props} /> <Icon size={48} />
<Text size='md'>{text}</Text>
</Group>
}
{...props}
/>
); );
} }
export default function Type({ file, popup = false, disableMediaPreview, ...props }){ export default function Type({ file, popup = false, disableMediaPreview, ...props }) {
const type = (file.type || file.mimetype).split('/')[0]; const type = (file.type || file.mimetype).split('/')[0];
const name = (file.name || file.file); const name = file.name || file.file;
const media = /^(video|audio|image|text)/.test(type); const media = /^(video|audio|image|text)/.test(type);
@ -36,18 +41,34 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
} }
if (media && disableMediaPreview) { if (media && disableMediaPreview) {
return <Placeholder Icon={FileIcon} text={`Click to view file (${name})`} disableResolve={true} {...props} />; return (
}; <Placeholder Icon={FileIcon} text={`Click to view file (${name})`} disableResolve={true} {...props} />
);
}
return popup ? (media ? { return popup ? (
'video': <video width='100%' autoPlay controls {...props} />, media ? (
'image': <Image {...props} />, {
'audio': <audio autoPlay controls {...props} style={{ width: '100%' }}/>, video: <video width='100%' autoPlay controls {...props} />,
'text': <Prism withLineNumbers language={name.split('.').pop()} {...props} style={{}} sx={{}}>{text}</Prism>, image: <Image {...props} />,
}[type]: <Text>Can&apos;t preview {file.type || file.mimetype}</Text>) : (media ? { audio: <audio autoPlay controls {...props} style={{ width: '100%' }} />,
'video': <Placeholder Icon={PlayIcon} text={`Click to view video (${name})`} {...props} />, text: (
'image': <Image {...props} />, <Prism withLineNumbers language={name.split('.').pop()} {...props} style={{}} sx={{}}>
'audio': <Placeholder Icon={AudioIcon} text={`Click to view audio (${name})`} {...props}/>, {text}
'text': <Placeholder Icon={FileIcon} text={`Click to view text file (${name})`} {...props}/>, </Prism>
}[type] : <Placeholder Icon={FileIcon} text={`Click to view file (${name})`} {...props}/>); ),
}; }[type]
) : (
<Text>Can&apos;t preview {file.type || file.mimetype}</Text>
)
) : media ? (
{
video: <Placeholder Icon={PlayIcon} text={`Click to view video (${name})`} {...props} />,
image: <Image {...props} />,
audio: <Placeholder Icon={AudioIcon} text={`Click to view audio (${name})`} {...props} />,
text: <Placeholder Icon={FileIcon} text={`Click to view text file (${name})`} {...props} />,
}[type]
) : (
<Placeholder Icon={FileIcon} text={`Click to view file (${name})`} {...props} />
);
}

View file

@ -16,9 +16,7 @@ export default function Dropzone({ loading, onDrop, children }) {
</Text> </Text>
</Group> </Group>
<div style={{ pointerEvents: 'all' }}> <div style={{ pointerEvents: 'all' }}>{children}</div>
{children}
</div>
</MantineDropzone> </MantineDropzone>
); );
} }

View file

@ -46,9 +46,7 @@ export default function FileDropzone({ file }: { file: File }) {
</div> </div>
} }
> >
<Badge size='lg'> <Badge size='lg'>{file.name}</Badge>
{file.name}
</Badge>
</Tooltip> </Tooltip>
); );
} }

View file

@ -2,4 +2,4 @@ import { Activity } from 'react-feather';
export default function ActivityIcon({ ...props }) { export default function ActivityIcon({ ...props }) {
return <Activity size={15} {...props} />; return <Activity size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Disc } from 'react-feather';
export default function AudioIcon({ ...props }) { export default function AudioIcon({ ...props }) {
return <Disc size={15} {...props} />; return <Disc size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Calendar } from 'react-feather';
export default function CalendarIcon({ ...props }) { export default function CalendarIcon({ ...props }) {
return <Calendar size={15} {...props} />; return <Calendar size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Check } from 'react-feather';
export default function CheckIcon({ ...props }) { export default function CheckIcon({ ...props }) {
return <Check size={15} {...props} />; return <Check size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Clock } from 'react-feather';
export default function ClockIcon({ ...props }) { export default function ClockIcon({ ...props }) {
return <Clock size={15} {...props} />; return <Clock size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Copy } from 'react-feather';
export default function CopyIcon({ ...props }) { export default function CopyIcon({ ...props }) {
return <Copy size={15} {...props} />; return <Copy size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { X } from 'react-feather';
export default function CrossIcon({ ...props }) { export default function CrossIcon({ ...props }) {
return <X size={15} {...props} />; return <X size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Delete } from 'react-feather';
export default function DeleteIcon({ ...props }) { export default function DeleteIcon({ ...props }) {
return <Delete size={15} {...props} />; return <Delete size={15} {...props} />;
} }

View file

@ -3,5 +3,12 @@
import Image from 'next/image'; import Image from 'next/image';
export default function DiscordIcon({ ...props }) { export default function DiscordIcon({ ...props }) {
return <Image src='https://assets-global.website-files.com/6257adef93867e50d84d30e2/62595384f934b806f37f4956_145dc557845548a36a82337912ca3ac5.svg' width={24} height={24} {...props} />; return (
} <Image
src='https://assets-global.website-files.com/6257adef93867e50d84d30e2/62595384f934b806f37f4956_145dc557845548a36a82337912ca3ac5.svg'
width={24}
height={24}
{...props}
/>
);
}

View file

@ -2,4 +2,4 @@ import { Download } from 'react-feather';
export default function DownloadIcon({ ...props }) { export default function DownloadIcon({ ...props }) {
return <Download size={15} {...props} />; return <Download size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { LogIn } from 'react-feather';
export default function EnterIcon({ ...props }) { export default function EnterIcon({ ...props }) {
return <LogIn size={15} {...props} />; return <LogIn size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { ExternalLink } from 'react-feather';
export default function ExternalLinkIcon({ ...props }) { export default function ExternalLinkIcon({ ...props }) {
return <ExternalLink size={15} {...props} />; return <ExternalLink size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Eye } from 'react-feather';
export default function EyeIcon({ ...props }) { export default function EyeIcon({ ...props }) {
return <Eye size={15} {...props} />; return <Eye size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { File } from 'react-feather';
export default function FileIcon({ ...props }) { export default function FileIcon({ ...props }) {
return <File size={15} {...props} />; return <File size={15} {...props} />;
} }

View file

@ -3,5 +3,12 @@
import Image from 'next/image'; import Image from 'next/image';
export default function FlameshotIcon({ ...props }) { export default function FlameshotIcon({ ...props }) {
return <Image src='https://raw.githubusercontent.com/flameshot-org/flameshot/master/data/img/app/flameshot.svg' width={24} height={24} {...props} />; return (
} <Image
src='https://raw.githubusercontent.com/flameshot-org/flameshot/master/data/img/app/flameshot.svg'
width={24}
height={24}
{...props}
/>
);
}

View file

@ -2,4 +2,4 @@ import { GitHub } from 'react-feather';
export default function GitHubIcon({ ...props }) { export default function GitHubIcon({ ...props }) {
return <GitHub size={24} {...props} />; return <GitHub size={24} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Hash } from 'react-feather';
export default function HashIcon({ ...props }) { export default function HashIcon({ ...props }) {
return <Hash size={15} {...props} />; return <Hash size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Home } from 'react-feather';
export default function HomeIcon({ ...props }) { export default function HomeIcon({ ...props }) {
return <Home size={15} {...props} />; return <Home size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Image as FeatherImage } from 'react-feather';
export default function ImageIcon({ ...props }) { export default function ImageIcon({ ...props }) {
return <FeatherImage size={15} {...props} />; return <FeatherImage size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Link } from 'react-feather';
export default function LinkIcon({ ...props }) { export default function LinkIcon({ ...props }) {
return <Link size={15} {...props} />; return <Link size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { LogOut } from 'react-feather';
export default function LogoutIcon({ ...props }) { export default function LogoutIcon({ ...props }) {
return <LogOut size={15} {...props} />; return <LogOut size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Edit2 } from 'react-feather';
export default function PencilIcon({ ...props }) { export default function PencilIcon({ ...props }) {
return <Edit2 size={15} {...props} />; return <Edit2 size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Play } from 'react-feather';
export default function PlayIcon({ ...props }) { export default function PlayIcon({ ...props }) {
return <Play size={15} {...props} />; return <Play size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Plus } from 'react-feather';
export default function PlusIcon({ ...props }) { export default function PlusIcon({ ...props }) {
return <Plus size={15} {...props} />; return <Plus size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { RefreshCw } from 'react-feather';
export default function RefreshIcon({ ...props }) { export default function RefreshIcon({ ...props }) {
return <RefreshCw size={15} {...props} />; return <RefreshCw size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Settings } from 'react-feather';
export default function SettingsIcon({ ...props }) { export default function SettingsIcon({ ...props }) {
return <Settings size={15} {...props} />; return <Settings size={15} {...props} />;
} }

View file

@ -4,4 +4,4 @@ import Image from 'next/image';
export default function ShareXIcon({ ...props }) { export default function ShareXIcon({ ...props }) {
return <Image src='https://getsharex.com/img/ShareX_Logo.svg' width={24} height={24} {...props} />; return <Image src='https://getsharex.com/img/ShareX_Logo.svg' width={24} height={24} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Star } from 'react-feather';
export default function StarIcon({ ...props }) { export default function StarIcon({ ...props }) {
return <Star size={15} {...props} />; return <Star size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Tag } from 'react-feather';
export default function TagIcon({ ...props }) { export default function TagIcon({ ...props }) {
return <Tag size={15} {...props} />; return <Tag size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Trash2 } from 'react-feather';
export default function TrashIcon({ ...props }) { export default function TrashIcon({ ...props }) {
return <Trash2 size={15} {...props} />; return <Trash2 size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Type } from 'react-feather';
export default function TypeIcon({ ...props }) { export default function TypeIcon({ ...props }) {
return <Type size={15} {...props} />; return <Type size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Upload } from 'react-feather';
export default function UploadIcon({ ...props }) { export default function UploadIcon({ ...props }) {
return <Upload size={15} {...props} />; return <Upload size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { User } from 'react-feather';
export default function UserIcon({ ...props }) { export default function UserIcon({ ...props }) {
return <User size={15} {...props} />; return <User size={15} {...props} />;
} }

View file

@ -2,4 +2,4 @@ import { Video } from 'react-feather';
export default function VideoIcon({ ...props }) { export default function VideoIcon({ ...props }) {
return <Video size={15} {...props} />; return <Video size={15} {...props} />;
} }

View file

@ -66,4 +66,4 @@ export {
DiscordIcon, DiscordIcon,
EyeIcon, EyeIcon,
RefreshIcon, RefreshIcon,
}; };

View file

@ -12,44 +12,43 @@ export default function RecentFiles({ disableMediaPreview }) {
<> <>
<Title>Recent Files</Title> <Title>Recent Files</Title>
<SimpleGrid <SimpleGrid
cols={(recent.isSuccess && recent.data.length === 0) ? 1 : 4} cols={recent.isSuccess && recent.data.length === 0 ? 1 : 4}
spacing='lg' spacing='lg'
breakpoints={[ breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
]}
> >
{ {recent.isSuccess ? (
recent.isSuccess recent.data.length > 0 ? (
? ( recent.data.map((image) => (
recent.data.length > 0 <File
? ( key={randomId()}
recent.data.map(image => ( image={image}
<File key={randomId()} image={image} updateImages={invalidateFiles} disableMediaPreview={disableMediaPreview} /> updateImages={invalidateFiles}
)) disableMediaPreview={disableMediaPreview}
) : ( />
<MantineCard shadow='md'> ))
<Center> ) : (
<Group> <MantineCard shadow='md'>
<div> <Center>
<UploadCloud size={48} /> <Group>
</div> <div>
<div> <UploadCloud size={48} />
<Title>Nothing here</Title> </div>
<MutedText size='md'>Upload some files and they will show up here.</MutedText> <div>
</div> <Title>Nothing here</Title>
</Group> <MutedText size='md'>Upload some files and they will show up here.</MutedText>
</Center> </div>
</MantineCard> </Group>
) </Center>
) : ( </MantineCard>
[1, 2, 3, 4].map(x => ( )
<div key={x}> ) : (
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }} /> [1, 2, 3, 4].map((x) => (
</div> <div key={x}>
)) <Skeleton width='100%' height={220} sx={{ borderRadius: 1 }} />
) </div>
} ))
)}
</SimpleGrid> </SimpleGrid>
</> </>
); );
} }

View file

@ -6,7 +6,7 @@ import { useStats } from 'lib/queries/stats';
import { Database, Eye, Users } from 'react-feather'; import { Database, Eye, Users } from 'react-feather';
export function StatCards() { export function StatCards() {
const stats = useStats(); const stats = useStats();
const latest = stats.data?.[0]; const latest = stats.data?.[0];
const before = stats.data?.[1]; const before = stats.data?.[1];
@ -18,44 +18,51 @@ export function StatCards() {
{ maxWidth: 'xs', cols: 1 }, { maxWidth: 'xs', cols: 1 },
]} ]}
> >
<StatCard stat={{ <StatCard
title: 'UPLOADED FILES', stat={{
value: stats.isSuccess ? latest.data.count.toLocaleString() : '...', title: 'UPLOADED FILES',
desc: 'files have been uploaded', value: stats.isSuccess ? latest.data.count.toLocaleString() : '...',
icon: ( desc: 'files have been uploaded',
<FileIcon /> icon: <FileIcon />,
), diff:
diff: stats.isSuccess && before?.data ? percentChange(before.data.count, latest.data.count) : undefined, stats.isSuccess && before?.data ? percentChange(before.data.count, latest.data.count) : undefined,
}}/> }}
/>
<StatCard stat={{ <StatCard
title: 'STORAGE', stat={{
value: stats.isSuccess ? latest.data.size : '...', title: 'STORAGE',
desc: 'of storage used', value: stats.isSuccess ? latest.data.size : '...',
icon: ( desc: 'of storage used',
<Database size={15} /> icon: <Database size={15} />,
), diff:
diff: stats.isSuccess && before?.data ? percentChange(before.data.size_num, latest.data.size_num) : undefined, stats.isSuccess && before?.data
}}/> ? percentChange(before.data.size_num, latest.data.size_num)
: undefined,
}}
/>
<StatCard stat={{ <StatCard
title: 'VIEWS', stat={{
value: stats.isSuccess ? latest.data.views_count.toLocaleString() : '...', title: 'VIEWS',
desc: 'total page views', value: stats.isSuccess ? latest.data.views_count.toLocaleString() : '...',
icon: ( desc: 'total page views',
<Eye size={15} /> icon: <Eye size={15} />,
), diff:
diff: stats.isSuccess && before?.data ? percentChange(before.data.views_count, latest.data.views_count) : undefined, stats.isSuccess && before?.data
}}/> ? percentChange(before.data.views_count, latest.data.views_count)
: undefined,
}}
/>
<StatCard stat={{ <StatCard
title: 'USERS', stat={{
value: stats.isSuccess ? latest.data.count_users.toLocaleString() : '...', title: 'USERS',
desc: 'total registered users', value: stats.isSuccess ? latest.data.count_users.toLocaleString() : '...',
icon: ( desc: 'total registered users',
<Users size={15} /> icon: <Users size={15} />,
), }}
}}/> />
</SimpleGrid> </SimpleGrid>
); );
} }

View file

@ -29,7 +29,9 @@ export default function Dashboard({ disableMediaPreview }) {
}; };
const deleteImage = async ({ original }) => { const deleteImage = async ({ original }) => {
const res = await useFetch('/api/user/files', 'DELETE', { id: original.id }); const res = await useFetch('/api/user/files', 'DELETE', {
id: original.id,
});
if (!res.error) { if (!res.error) {
updateImages(); updateImages();
showNotification({ showNotification({
@ -46,7 +48,6 @@ export default function Dashboard({ disableMediaPreview }) {
icon: <CrossIcon />, icon: <CrossIcon />,
}); });
} }
}; };
const copyImage = async ({ original }) => { const copyImage = async ({ original }) => {
@ -65,7 +66,9 @@ export default function Dashboard({ disableMediaPreview }) {
return ( return (
<div> <div>
<Title>Welcome back, {user?.username}</Title> <Title>Welcome back, {user?.username}</Title>
<MutedText size='md'>You have <b>{images.isSuccess ? images.data.length : '...'}</b> files</MutedText> <MutedText size='md'>
You have <b>{images.isSuccess ? images.data.length : '...'}</b> files
</MutedText>
<StatCards /> <StatCards />
@ -73,7 +76,9 @@ export default function Dashboard({ disableMediaPreview }) {
<section> <section>
<Title>Files</Title> <Title>Files</Title>
<MutedText size='md'>View your gallery <Link href='/dashboard/files'>here</Link>.</MutedText> <MutedText size='md'>
View your gallery <Link href='/dashboard/files'>here</Link>.
</MutedText>
<DataGrid <DataGrid
data={images.data ?? []} data={images.data ?? []}
loading={images.isLoading} loading={images.isLoading}
@ -124,7 +129,6 @@ export default function Dashboard({ disableMediaPreview }) {
}, },
}} }}
empty={<></>} empty={<></>}
columns={[ columns={[
{ {
accessorKey: 'file', accessorKey: 'file',
@ -146,4 +150,4 @@ export default function Dashboard({ disableMediaPreview }) {
</section> </section>
</div> </div>
); );
} }

View file

@ -29,35 +29,26 @@ export default function FilePagation({ disableMediaPreview }) {
return ( return (
<> <>
<SimpleGrid <SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
cols={3} {pages.isSuccess
spacing='lg' ? pages.data.length
breakpoints={[ ? pages.data[page - 1 ?? 0].map((image) => (
{ maxWidth: 'sm', cols: 1, spacing: 'sm' }, <div key={image.id}>
]} <File
> image={image}
{ updateImages={() => pages.refetch()}
(pages.isSuccess) disableMediaPreview={disableMediaPreview}
? pages.data.length />
? (
pages.data[(page - 1) ?? 0].map(image => (
<div key={image.id}>
<File image={image} updateImages={() => pages.refetch()} disableMediaPreview={disableMediaPreview} />
</div>
))
) : (
null
)
: (
[1,2,3,4].map(x => (
<div key={x}>
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }}/>
</div> </div>
)) ))
) : null
} : [1, 2, 3, 4].map((x) => (
<div key={x}>
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }} />
</div>
))}
</SimpleGrid> </SimpleGrid>
{(pages.isSuccess && pages.data.length) ? ( {pages.isSuccess && pages.data.length ? (
<Box <Box
sx={{ sx={{
display: 'flex', display: 'flex',
@ -68,10 +59,14 @@ export default function FilePagation({ disableMediaPreview }) {
}} }}
> >
<div></div> <div></div>
<Pagination total={pages.data?.length ?? 0} page={page} onChange={setPage}/> <Pagination total={pages.data?.length ?? 0} page={page} onChange={setPage} />
<Checkbox label='Show non-media files' checked={checked} onChange={(event) => setChecked(event.currentTarget.checked)} /> <Checkbox
label='Show non-media files'
checked={checked}
onChange={(event) => setChecked(event.currentTarget.checked)}
/>
</Box> </Box>
) : null} ) : null}
</> </>
); );
} }

View file

@ -11,12 +11,12 @@ export default function Files({ disableMediaPreview }) {
const favoritePages = usePaginatedFiles({ favorite: 'media' }); const favoritePages = usePaginatedFiles({ favorite: 'media' });
const [favoritePage, setFavoritePage] = useState(1); const [favoritePage, setFavoritePage] = useState(1);
const updatePages = async favorite => { const updatePages = async (favorite) => {
pages.refetch(); pages.refetch();
if (favorite) { if (favorite) {
favoritePages.refetch(); favoritePages.refetch();
} }
}; };
return ( return (
@ -24,50 +24,50 @@ export default function Files({ disableMediaPreview }) {
<Group mb='md'> <Group mb='md'>
<Title>Files</Title> <Title>Files</Title>
<Link href='/dashboard/upload' passHref> <Link href='/dashboard/upload' passHref>
<ActionIcon component='a' variant='filled' color='primary'><PlusIcon/></ActionIcon> <ActionIcon component='a' variant='filled' color='primary'>
<PlusIcon />
</ActionIcon>
</Link> </Link>
</Group> </Group>
{ {favoritePages.isSuccess && favoritePages.data.length ? (
(favoritePages.isSuccess && favoritePages.data.length) <Accordion variant='contained' mb='sm'>
? ( <Accordion.Item value='favorite'>
<Accordion <Accordion.Control>Favorite Files</Accordion.Control>
variant='contained' <Accordion.Panel>
mb='sm' <SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
> {favoritePages.isSuccess && favoritePages.data.length
<Accordion.Item value='favorite'> ? favoritePages.data[favoritePage - 1 ?? 0].map((image) => (
<Accordion.Control>Favorite Files</Accordion.Control>
<Accordion.Panel>
<SimpleGrid
cols={3}
spacing='lg'
breakpoints={[
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
]}
>
{(favoritePages.isSuccess && favoritePages.data.length) ? favoritePages.data[(favoritePage - 1) ?? 0].map(image => (
<div key={image.id}> <div key={image.id}>
<File image={image} updateImages={() => updatePages(true)} disableMediaPreview={disableMediaPreview} /> <File
image={image}
updateImages={() => updatePages(true)}
disableMediaPreview={disableMediaPreview}
/>
</div> </div>
)) : null} ))
</SimpleGrid> : null}
<Box </SimpleGrid>
sx={{ <Box
display: 'flex', sx={{
justifyContent: 'center', display: 'flex',
alignItems: 'center', justifyContent: 'center',
paddingTop: 12, alignItems: 'center',
paddingBottom: 3, paddingTop: 12,
}} paddingBottom: 3,
> }}
<Pagination total={favoritePages.data.length} page={favoritePage} onChange={setFavoritePage} /> >
</Box> <Pagination
</Accordion.Panel> total={favoritePages.data.length}
</Accordion.Item> page={favoritePage}
</Accordion> onChange={setFavoritePage}
) : null />
} </Box>
</Accordion.Panel>
</Accordion.Item>
</Accordion>
) : null}
<FilePagation disableMediaPreview={disableMediaPreview} /> <FilePagation disableMediaPreview={disableMediaPreview} />
</> </>
); );
} }

View file

@ -1,4 +1,17 @@
import { ActionIcon, Avatar, Button, Card, Group, Modal, NumberInput, Select, SimpleGrid, Skeleton, Stack, Title } from '@mantine/core'; import {
ActionIcon,
Avatar,
Button,
Card,
Group,
Modal,
NumberInput,
Select,
SimpleGrid,
Skeleton,
Stack,
Title,
} from '@mantine/core';
import { useClipboard } from '@mantine/hooks'; import { useClipboard } from '@mantine/hooks';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { useModals } from '@mantine/modals'; import { useModals } from '@mantine/modals';
@ -9,17 +22,7 @@ import useFetch from 'hooks/useFetch';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
const expires = [ const expires = ['30m', '1h', '6h', '12h', '1d', '3d', '5d', '7d', 'never'];
'30m',
'1h',
'6h',
'12h',
'1d',
'3d',
'5d',
'7d',
'never',
];
function CreateInviteModal({ open, setOpen, updateInvites }) { function CreateInviteModal({ open, setOpen, updateInvites }) {
const form = useForm({ const form = useForm({
@ -29,18 +32,24 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
}, },
}); });
const onSubmit = async values => { const onSubmit = async (values) => {
if (!expires.includes(values.expires)) return form.setFieldError('expires', 'Invalid expiration'); if (!expires.includes(values.expires)) return form.setFieldError('expires', 'Invalid expiration');
if (values.count < 1 || values.count > 100) return form.setFieldError('count', 'Must be between 1 and 100'); if (values.count < 1 || values.count > 100)
const expires_at = values.expires === 'never' ? null : new Date({ return form.setFieldError('count', 'Must be between 1 and 100');
'30m': Date.now() + 30 * 60 * 1000, const expires_at =
'1h': Date.now() + 60 * 60 * 1000, values.expires === 'never'
'6h': Date.now() + 6 * 60 * 60 * 1000, ? null
'12h': Date.now() + 12 * 60 * 60 * 1000, : new Date(
'1d': Date.now() + 24 * 60 * 60 * 1000, {
'5d': Date.now() + 5 * 24 * 60 * 60 * 1000, '30m': Date.now() + 30 * 60 * 1000,
'7d': Date.now() + 7 * 24 * 60 * 60 * 1000, '1h': Date.now() + 60 * 60 * 1000,
}[values.expires]); '6h': Date.now() + 6 * 60 * 60 * 1000,
'12h': Date.now() + 12 * 60 * 60 * 1000,
'1d': Date.now() + 24 * 60 * 60 * 1000,
'5d': Date.now() + 5 * 24 * 60 * 60 * 1000,
'7d': Date.now() + 7 * 24 * 60 * 60 * 1000,
}[values.expires]
);
setOpen(false); setOpen(false);
@ -69,12 +78,8 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
}; };
return ( return (
<Modal <Modal opened={open} onClose={() => setOpen(false)} title={<Title>Create Invite</Title>}>
opened={open} <form onSubmit={form.onSubmit((v) => onSubmit(v))}>
onClose={() => setOpen(false)}
title={<Title>Create Invite</Title>}
>
<form onSubmit={form.onSubmit(v => onSubmit(v))}>
<Select <Select
label='Expires' label='Expires'
id='expires' id='expires'
@ -100,7 +105,7 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
min={1} min={1}
stepHoldDelay={200} stepHoldDelay={200}
stepHoldInterval={100} stepHoldInterval={100}
parser={(v:string) => Number(v.replace(/[^\d]/g, ''))} parser={(v: string) => Number(v.replace(/[^\d]/g, ''))}
/> />
<Group position='right' mt={22}> <Group position='right' mt={22}>
@ -120,34 +125,35 @@ export default function Users() {
const [invites, setInvites] = useState([]); const [invites, setInvites] = useState([]);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const openDeleteModal = invite => modals.openConfirmModal({ const openDeleteModal = (invite) =>
title: `Delete ${invite.code}?`, modals.openConfirmModal({
centered: true, title: `Delete ${invite.code}?`,
overlayBlur: 3, centered: true,
labels: { confirm: 'Yes', cancel: 'No' }, overlayBlur: 3,
onConfirm: async () => { labels: { confirm: 'Yes', cancel: 'No' },
const res = await useFetch(`/api/auth/invite?code=${invite.code}`, 'DELETE'); onConfirm: async () => {
if (res.error) { const res = await useFetch(`/api/auth/invite?code=${invite.code}`, 'DELETE');
showNotification({ if (res.error) {
title: 'Failed to delete invite ${invite.code}', showNotification({
message: res.error, title: 'Failed to delete invite ${invite.code}',
icon: <CrossIcon />, message: res.error,
color: 'red', icon: <CrossIcon />,
}); color: 'red',
} else { });
showNotification({ } else {
title: `Deleted invite ${invite.code}`, showNotification({
message: '', title: `Deleted invite ${invite.code}`,
icon: <DeleteIcon />, message: '',
color: 'green', icon: <DeleteIcon />,
}); color: 'green',
} });
}
updateInvites(); updateInvites();
}, },
}); });
const handleCopy = async invite => { const handleCopy = async (invite) => {
clipboard.copy(`${window.location.protocol}//${window.location.host}/invite/${invite.code}`); clipboard.copy(`${window.location.protocol}//${window.location.host}/invite/${invite.code}`);
showNotification({ showNotification({
title: 'Copied to clipboard', title: 'Copied to clipboard',
@ -162,7 +168,7 @@ export default function Users() {
setInvites(us); setInvites(us);
} else { } else {
router.push('/dashboard'); router.push('/dashboard');
}; }
}; };
useEffect(() => { useEffect(() => {
@ -174,39 +180,42 @@ export default function Users() {
<CreateInviteModal open={open} setOpen={setOpen} updateInvites={updateInvites} /> <CreateInviteModal open={open} setOpen={setOpen} updateInvites={updateInvites} />
<Group mb='md'> <Group mb='md'>
<Title>Invites</Title> <Title>Invites</Title>
<ActionIcon variant='filled' color='primary' onClick={() => setOpen(true)}><PlusIcon /></ActionIcon> <ActionIcon variant='filled' color='primary' onClick={() => setOpen(true)}>
<PlusIcon />
</ActionIcon>
</Group> </Group>
<SimpleGrid <SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
cols={3} {invites.length
spacing='lg' ? invites.map((invite) => (
breakpoints={[ <Card key={invite.id} sx={{ maxWidth: '100%' }}>
{ maxWidth: 'sm', cols: 1, spacing: 'sm' }, <Group position='apart'>
]} <Group position='left'>
> <Avatar size='lg' color={invite.used ? 'dark' : 'primary'}>
{invites.length ? invites.map(invite => ( {invite.id}
<Card key={invite.id} sx={{ maxWidth: '100%' }}> </Avatar>
<Group position='apart'> <Stack spacing={0}>
<Group position='left'> <Title>
<Avatar size='lg' color={invite.used ? 'dark' : 'primary'}>{invite.id}</Avatar> {invite.code}
<Stack spacing={0}> {invite.used && <> (Used)</>}
<Title>{invite.code}{invite.used && <> (Used)</>}</Title> </Title>
<MutedText size='sm'>Created: {new Date(invite.created_at).toLocaleString()}</MutedText> <MutedText size='sm'>Created: {new Date(invite.created_at).toLocaleString()}</MutedText>
<MutedText size='sm'>Expires: {invite.expires_at ? new Date(invite.expires_at).toLocaleString() : 'Never'}</MutedText> <MutedText size='sm'>
</Stack> Expires: {invite.expires_at ? new Date(invite.expires_at).toLocaleString() : 'Never'}
</Group> </MutedText>
<Group position='right'> </Stack>
<ActionIcon aria-label='copy' onClick={() => handleCopy(invite)}> </Group>
<CopyIcon /> <Group position='right'>
</ActionIcon> <ActionIcon aria-label='copy' onClick={() => handleCopy(invite)}>
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(invite)}> <CopyIcon />
<DeleteIcon /> </ActionIcon>
</ActionIcon> <ActionIcon aria-label='delete' onClick={() => openDeleteModal(invite)}>
</Group> <DeleteIcon />
</Group> </ActionIcon>
</Card> </Group>
)) : [1, 2, 3].map(x => ( </Group>
<Skeleton key={x} width='100%' height={100} radius='sm' /> </Card>
))} ))
: [1, 2, 3].map((x) => <Skeleton key={x} width='100%' height={100} radius='sm' />)}
</SimpleGrid> </SimpleGrid>
</> </>
); );

View file

@ -1,7 +1,7 @@
import { GeneratorModal } from './GeneratorModal'; import { GeneratorModal } from './GeneratorModal';
export default function Flameshot({ user, open, setOpen }) { export default function Flameshot({ user, open, setOpen }) {
const onSubmit = values => { const onSubmit = (values) => {
const curl = [ const curl = [
'curl', 'curl',
'-H', '-H',
@ -10,7 +10,12 @@ export default function Flameshot({ user, open, setOpen }) {
`"authorization: ${user?.token}"`, `"authorization: ${user?.token}"`,
'-F', '-F',
'file=@/tmp/ss.png', 'file=@/tmp/ss.png',
`${window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '')}/api/upload`, `${
window.location.protocol +
'//' +
window.location.hostname +
(window.location.port ? ':' + window.location.port : '')
}/api/upload`,
]; ];
const extraHeaders = {}; const extraHeaders = {};
@ -58,11 +63,13 @@ ${curl.join(' ')} | jq -r '.files[0]' | tr -d '\n' | xsel -ib;
pseudoElement.parentNode.removeChild(pseudoElement); pseudoElement.parentNode.removeChild(pseudoElement);
}; };
return <GeneratorModal return (
opened={open} <GeneratorModal
onClose={() => setOpen(false)} opened={open}
title='Flameshot' onClose={() => setOpen(false)}
desc='To use this script, you need Flameshot, curl, jq, and xsel installed. This script is intended for use on Linux only.' title='Flameshot'
onSubmit={onSubmit} desc='To use this script, you need Flameshot, curl, jq, and xsel installed. This script is intended for use on Linux only.'
/>; onSubmit={onSubmit}
} />
);
}

View file

@ -11,16 +11,11 @@ export function GeneratorModal({ opened, onClose, title, onSubmit, ...other }) {
embed: false, embed: false,
}, },
}); });
return ( return (
<Modal <Modal opened={opened} onClose={onClose} title={<Title order={3}>{title}</Title>} size='lg'>
opened={opened}
onClose={onClose}
title={<Title order={3}>{title}</Title>}
size='lg'
>
{other.desc && <Text>{other.desc}</Text>} {other.desc && <Text>{other.desc}</Text>}
<form onSubmit={form.onSubmit(values => onSubmit(values))}> <form onSubmit={form.onSubmit((values) => onSubmit(values))}>
<Select <Select
label='Select file name format' label='Select file name format'
data={[ data={[
@ -34,7 +29,7 @@ export function GeneratorModal({ opened, onClose, title, onSubmit, ...other }) {
/> />
<NumberInput <NumberInput
label={'Image Compression (leave at 0 if you don\'t want to compress)'} label={"Image Compression (leave at 0 if you don't want to compress)"}
max={100} max={100}
min={0} min={0}
mt='md' mt='md'
@ -48,30 +43,19 @@ export function GeneratorModal({ opened, onClose, title, onSubmit, ...other }) {
id='zeroWidthSpace' id='zeroWidthSpace'
{...form.getInputProps('zeroWidthSpace', { type: 'checkbox' })} {...form.getInputProps('zeroWidthSpace', { type: 'checkbox' })}
/> />
<Checkbox <Checkbox label='Embed' id='embed' {...form.getInputProps('embed', { type: 'checkbox' })} />
label='Embed'
id='embed'
{...form.getInputProps('embed', { type: 'checkbox' })}
/>
</Group> </Group>
<Group grow> <Group grow>
<Button <Button mt='md' onClick={form.reset}>
mt='md'
onClick={form.reset}
>
Reset Reset
</Button> </Button>
<Button <Button mt='md' rightIcon={<DownloadIcon />} type='submit'>
mt='md'
rightIcon={<DownloadIcon />}
type='submit'
>
Download Download
</Button> </Button>
</Group> </Group>
</form> </form>
</Modal> </Modal>
); );
} }

View file

@ -7,7 +7,12 @@ export default function ShareX({ user, open, setOpen }) {
Name: 'Zipline', Name: 'Zipline',
DestinationType: 'ImageUploader, TextUploader', DestinationType: 'ImageUploader, TextUploader',
RequestMethod: 'POST', RequestMethod: 'POST',
RequestURL: `${window.location.protocol + '//' + window.location.hostname + (window.location.port ? ':' + window.location.port : '')}/api/upload`, RequestURL: `${
window.location.protocol +
'//' +
window.location.hostname +
(window.location.port ? ':' + window.location.port : '')
}/api/upload`,
Headers: { Headers: {
Authorization: user?.token, Authorization: user?.token,
}, },
@ -16,7 +21,7 @@ export default function ShareX({ user, open, setOpen }) {
FileFormName: 'file', FileFormName: 'file',
}); });
const onSubmit = values => { const onSubmit = (values) => {
if (values.format !== 'RANDOM') { if (values.format !== 'RANDOM') {
config.Headers['Format'] = values.format; config.Headers['Format'] = values.format;
setConfig(config); setConfig(config);
@ -50,7 +55,10 @@ export default function ShareX({ user, open, setOpen }) {
} }
const pseudoElement = document.createElement('a'); const pseudoElement = document.createElement('a');
pseudoElement.setAttribute('href', 'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(config, null, '\t'))); pseudoElement.setAttribute(
'href',
'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(config, null, '\t'))
);
pseudoElement.setAttribute('download', 'zipline.sxcu'); pseudoElement.setAttribute('download', 'zipline.sxcu');
pseudoElement.style.display = 'none'; pseudoElement.style.display = 'none';
document.body.appendChild(pseudoElement); document.body.appendChild(pseudoElement);
@ -58,10 +66,5 @@ export default function ShareX({ user, open, setOpen }) {
pseudoElement.parentNode.removeChild(pseudoElement); pseudoElement.parentNode.removeChild(pseudoElement);
}; };
return <GeneratorModal return <GeneratorModal opened={open} onClose={() => setOpen(false)} title='ShareX' onSubmit={onSubmit} />;
opened={open} }
onClose={() => setOpen(false)}
title='ShareX'
onSubmit={onSubmit}
/>;
}

View file

@ -1,9 +1,30 @@
import { Box, Button, Card, ColorInput, FileInput, Group, Image, PasswordInput, Space, Text, TextInput, Title, Tooltip } from '@mantine/core'; import {
Box,
Button,
Card,
ColorInput,
FileInput,
Group,
Image,
PasswordInput,
Space,
Text,
TextInput,
Title,
Tooltip,
} from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { randomId, useInterval } from '@mantine/hooks'; import { randomId, useInterval } from '@mantine/hooks';
import { useModals } from '@mantine/modals'; import { useModals } from '@mantine/modals';
import { showNotification, updateNotification } from '@mantine/notifications'; import { showNotification, updateNotification } from '@mantine/notifications';
import { CrossIcon, DeleteIcon, FlameshotIcon, RefreshIcon, SettingsIcon, ShareXIcon } from 'components/icons'; import {
CrossIcon,
DeleteIcon,
FlameshotIcon,
RefreshIcon,
SettingsIcon,
ShareXIcon,
} from 'components/icons';
import DownloadIcon from 'components/icons/DownloadIcon'; import DownloadIcon from 'components/icons/DownloadIcon';
import Link from 'components/Link'; import Link from 'components/Link';
import MutedText from 'components/MutedText'; import MutedText from 'components/MutedText';
@ -17,7 +38,15 @@ import Flameshot from './Flameshot';
import ShareX from './ShareX'; import ShareX from './ShareX';
function ExportDataTooltip({ children }) { function ExportDataTooltip({ children }) {
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>; return (
<Tooltip
position='top'
color=''
label='After clicking, if you have a lot of files the export can take a while to complete. A list of previous exports will be below to download.'
>
{children}
</Tooltip>
);
} }
export default function Manage() { export default function Manage() {
@ -70,7 +99,7 @@ export default function Manage() {
if (newUser.error) { if (newUser.error) {
updateNotification({ updateNotification({
id: 'update-user', id: 'update-user',
title: 'Couldn\'t save user', title: "Couldn't save user",
message: newUser.error, message: newUser.error,
color: 'red', color: 'red',
icon: <CrossIcon />, icon: <CrossIcon />,
@ -96,14 +125,14 @@ export default function Manage() {
}, },
}); });
const onSubmit = async values => { const onSubmit = async (values) => {
const cleanUsername = values.username.trim(); const cleanUsername = values.username.trim();
const cleanPassword = values.password.trim(); const cleanPassword = values.password.trim();
const cleanEmbedTitle = values.embedTitle.trim(); const cleanEmbedTitle = values.embedTitle.trim();
const cleanEmbedColor = values.embedColor.trim(); const cleanEmbedColor = values.embedColor.trim();
const cleanEmbedSiteName = values.embedSiteName.trim(); const cleanEmbedSiteName = values.embedSiteName.trim();
if (cleanUsername === '') return form.setFieldError('username', 'Username can\'t be nothing'); if (cleanUsername === '') return form.setFieldError('username', "Username can't be nothing");
showNotification({ showNotification({
id: 'update-user', id: 'update-user',
@ -119,7 +148,10 @@ export default function Manage() {
embedTitle: cleanEmbedTitle === '' ? null : cleanEmbedTitle, embedTitle: cleanEmbedTitle === '' ? null : cleanEmbedTitle,
embedColor: cleanEmbedColor === '' ? null : cleanEmbedColor, embedColor: cleanEmbedColor === '' ? null : cleanEmbedColor,
embedSiteName: cleanEmbedSiteName === '' ? null : cleanEmbedSiteName, embedSiteName: cleanEmbedSiteName === '' ? null : cleanEmbedSiteName,
domains: values.domains.split(/\s?,\s?/).map(x => x.trim()).filter(x => x !== ''), domains: values.domains
.split(/\s?,\s?/)
.map((x) => x.trim())
.filter((x) => x !== ''),
}; };
const newUser = await useFetch('/api/user', 'PATCH', data); const newUser = await useFetch('/api/user', 'PATCH', data);
@ -128,22 +160,26 @@ export default function Manage() {
if (newUser.invalidDomains) { if (newUser.invalidDomains) {
updateNotification({ updateNotification({
id: 'update-user', id: 'update-user',
message: <> message: (
<Text mt='xs'>The following domains are invalid:</Text> <>
{newUser.invalidDomains.map(err => ( <Text mt='xs'>The following domains are invalid:</Text>
<> {newUser.invalidDomains.map((err) => (
<Text color='gray' key={randomId()}>{err.domain}: {err.reason}</Text> <>
<Space h='md' /> <Text color='gray' key={randomId()}>
</> {err.domain}: {err.reason}
))} </Text>
</>, <Space h='md' />
</>
))}
</>
),
color: 'red', color: 'red',
icon: <CrossIcon />, icon: <CrossIcon />,
}); });
} }
updateNotification({ updateNotification({
id: 'update-user', id: 'update-user',
title: 'Couldn\'t save user', title: "Couldn't save user",
message: newUser.error, message: newUser.error,
color: 'red', color: 'red',
icon: <CrossIcon />, icon: <CrossIcon />,
@ -164,7 +200,8 @@ export default function Manage() {
showNotification({ showNotification({
title: 'Export started...', title: 'Export started...',
loading: true, loading: true,
message: 'If you have a lot of files, the export may take a while. The list of exports will be updated every 30s.', message:
'If you have a lot of files, the export may take a while. The list of exports will be updated every 30s.',
}); });
} else { } else {
showNotification({ showNotification({
@ -179,11 +216,15 @@ export default function Manage() {
const getExports = async () => { const getExports = async () => {
const res = await useFetch('/api/user/export'); const res = await useFetch('/api/user/export');
setExports(res.exports.map(s => ({ setExports(
date: new Date(Number(s.name.split('_')[3].slice(0, -4))), res.exports
size: s.size, .map((s) => ({
full: s.name, date: new Date(Number(s.name.split('_')[3].slice(0, -4))),
})).sort((a, b) => a.date.getTime() - b.date.getTime())); size: s.size,
full: s.name,
}))
.sort((a, b) => a.date.getTime() - b.date.getTime())
);
}; };
const handleDelete = async () => { const handleDelete = async () => {
@ -193,7 +234,7 @@ export default function Manage() {
if (!res.count) { if (!res.count) {
showNotification({ showNotification({
title: 'Couldn\'t delete files', title: "Couldn't delete files",
message: res.error, message: res.error,
color: 'red', color: 'red',
icon: <CrossIcon />, icon: <CrossIcon />,
@ -208,24 +249,25 @@ export default function Manage() {
} }
}; };
const openDeleteModal = () => modals.openConfirmModal({ const openDeleteModal = () =>
title: 'Are you sure you want to delete all of your files?', modals.openConfirmModal({
closeOnConfirm: false, title: 'Are you sure you want to delete all of your files?',
labels: { confirm: 'Yes', cancel: 'No' }, closeOnConfirm: false,
onConfirm: () => { labels: { confirm: 'Yes', cancel: 'No' },
modals.openConfirmModal({ onConfirm: () => {
title: 'Are you really sure?', modals.openConfirmModal({
labels: { confirm: 'Yes', cancel: 'No' }, title: 'Are you really sure?',
onConfirm: () => { labels: { confirm: 'Yes', cancel: 'No' },
handleDelete(); onConfirm: () => {
modals.closeAll(); handleDelete();
}, modals.closeAll();
onCancel: () => { },
modals.closeAll(); onCancel: () => {
}, modals.closeAll();
}); },
}, });
}); },
});
const interval = useInterval(() => getExports(), 30000); const interval = useInterval(() => getExports(), 30000);
useEffect(() => { useEffect(() => {
@ -236,30 +278,49 @@ export default function Manage() {
return ( return (
<> <>
<Title>Manage User</Title> <Title>Manage User</Title>
<MutedText size='md'>Want to use variables in embed text? Visit <Link href='https://zipline.diced.tech/docs/guides/variables'>the docs</Link> for variables</MutedText> <MutedText size='md'>
Want to use variables in embed text? Visit{' '}
<Link href='https://zipline.diced.tech/docs/guides/variables'>the docs</Link> for variables
</MutedText>
<form onSubmit={form.onSubmit((v) => onSubmit(v))}> <form onSubmit={form.onSubmit((v) => onSubmit(v))}>
<TextInput id='username' label='Username' {...form.getInputProps('username')} /> <TextInput id='username' label='Username' {...form.getInputProps('username')} />
<PasswordInput id='password' label='Password' description='Leave blank to keep your old password' {...form.getInputProps('password')} /> <PasswordInput
id='password'
label='Password'
description='Leave blank to keep your old password'
{...form.getInputProps('password')}
/>
<TextInput id='embedTitle' label='Embed Title' {...form.getInputProps('embedTitle')} /> <TextInput id='embedTitle' label='Embed Title' {...form.getInputProps('embedTitle')} />
<ColorInput id='embedColor' label='Embed Color' {...form.getInputProps('embedColor')} /> <ColorInput id='embedColor' label='Embed Color' {...form.getInputProps('embedColor')} />
<TextInput id='embedSiteName' label='Embed Site Name' {...form.getInputProps('embedSiteName')} /> <TextInput id='embedSiteName' label='Embed Site Name' {...form.getInputProps('embedSiteName')} />
<TextInput id='domains' label='Domains' description='A list of domains separated by commas. These domains will be used to randomly output a domain when uploading. This is optional.' placeholder='https://example.com, https://example2.com' {...form.getInputProps('domains')} /> <TextInput
id='domains'
label='Domains'
description='A list of domains separated by commas. These domains will be used to randomly output a domain when uploading. This is optional.'
placeholder='https://example.com, https://example2.com'
{...form.getInputProps('domains')}
/>
<Group position='right' mt='md'> <Group position='right' mt='md'>
<Button <Button type='submit'>Save User</Button>
type='submit'
>Save User</Button>
</Group> </Group>
</form> </form>
<Box mb='md'> <Box mb='md'>
<Title>Avatar</Title> <Title>Avatar</Title>
<FileInput placeholder='Click to upload a file' id='file' description='Add a custom avatar or leave blank for none' accept='image/png,image/jpeg,image/gif' value={file} onChange={handleAvatarChange} /> <FileInput
placeholder='Click to upload a file'
id='file'
description='Add a custom avatar or leave blank for none'
accept='image/png,image/jpeg,image/gif'
value={file}
onChange={handleAvatarChange}
/>
<Card mt='md'> <Card mt='md'>
<Text>Preview:</Text> <Text>Preview:</Text>
<Button <Button
leftIcon={fileDataURL ? <Image src={fileDataURL} height={32} radius='md' /> : <SettingsIcon />} leftIcon={fileDataURL ? <Image src={fileDataURL} height={32} radius='md' /> : <SettingsIcon />}
sx={t => ({ sx={(t) => ({
backgroundColor: '#00000000', backgroundColor: '#00000000',
'&:hover': { '&:hover': {
backgroundColor: t.other.hover, backgroundColor: t.other.hover,
@ -273,8 +334,16 @@ export default function Manage() {
</Card> </Card>
<Group position='right' mt='md'> <Group position='right' mt='md'>
<Button onClick={() => { setFile(null); setFileDataURL(null); }} color='red'>Reset</Button> <Button
<Button onClick={saveAvatar} >Save Avatar</Button> onClick={() => {
setFile(null);
setFileDataURL(null);
}}
color='red'
>
Reset
</Button>
<Button onClick={saveAvatar}>Save Avatar</Button>
</Group> </Group>
</Box> </Box>
@ -284,9 +353,17 @@ export default function Manage() {
</Box> </Box>
<Group> <Group>
<Button onClick={openDeleteModal} rightIcon={<DeleteIcon />} color='red'>Delete All Data</Button> <Button onClick={openDeleteModal} rightIcon={<DeleteIcon />} color='red'>
<ExportDataTooltip><Button onClick={exportData} rightIcon={<DownloadIcon />}>Export Data</Button></ExportDataTooltip> Delete All Data
<Button onClick={getExports} rightIcon={<RefreshIcon />}>Refresh</Button> </Button>
<ExportDataTooltip>
<Button onClick={exportData} rightIcon={<DownloadIcon />}>
Export Data
</Button>
</ExportDataTooltip>
<Button onClick={getExports} rightIcon={<RefreshIcon />}>
Refresh
</Button>
</Group> </Group>
<Card mt={22}> <Card mt={22}>
{exports && exports.length ? ( {exports && exports.length ? (
@ -296,11 +373,16 @@ export default function Manage() {
{ id: 'date', name: 'Date' }, { id: 'date', name: 'Date' },
{ id: 'size', name: 'Size' }, { id: 'size', name: 'Size' },
]} ]}
rows={exports ? exports.map((x, i) => ({ rows={
name: <Link href={'/api/user/export?name=' + x.full}>Export {i + 1}</Link>, exports
date: x.date.toLocaleString(), ? exports.map((x, i) => ({
size: bytesToRead(x.size), 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> <Text>No exports yet</Text>
)} )}
@ -308,8 +390,12 @@ export default function Manage() {
<Title my='md'>Uploaders</Title> <Title my='md'>Uploaders</Title>
<Group> <Group>
<Button size='xl' onClick={() => setShareXOpen(true)} rightIcon={<ShareXIcon />}>Generate ShareX Config</Button> <Button size='xl' onClick={() => setShareXOpen(true)} rightIcon={<ShareXIcon />}>
<Button size='xl' onClick={() => setFlameshotOpen(true)} rightIcon={<FlameshotIcon />}>Generate Flameshot Script</Button> Generate ShareX Config
</Button>
<Button size='xl' onClick={() => setFlameshotOpen(true)} rightIcon={<FlameshotIcon />}>
Generate Flameshot Script
</Button>
</Group> </Group>
<ShareX user={user} open={shareXOpen} setOpen={setShareXOpen} /> <ShareX user={user} open={shareXOpen} setOpen={setShareXOpen} />

View file

@ -1,5 +1,16 @@
import { Box, Card, Grid, LoadingOverlay, MantineTheme, Title, useMantineTheme } from '@mantine/core'; import { Box, Card, Grid, LoadingOverlay, MantineTheme, Title, useMantineTheme } from '@mantine/core';
import { ArcElement, CategoryScale, Chart as ChartJS, ChartData, ChartOptions, LinearScale, LineController, LineElement, PointElement, Tooltip } from 'chart.js'; import {
ArcElement,
CategoryScale,
Chart as ChartJS,
ChartData,
ChartOptions,
LinearScale,
LineController,
LineElement,
PointElement,
Tooltip,
} from 'chart.js';
import ChartDataLabels from 'chartjs-plugin-datalabels'; import ChartDataLabels from 'chartjs-plugin-datalabels';
import ColorHash from 'color-hash'; import ColorHash from 'color-hash';
import { bytesToRead } from 'lib/utils/client'; import { bytesToRead } from 'lib/utils/client';
@ -49,13 +60,12 @@ const CHART_OPTIONS = (theme: MantineTheme): ChartOptions => ({
}, },
}); });
type LineChartData = ChartData<'line', number[], string>; type LineChartData = ChartData<'line', number[], string>;
type ChartDataMemo = { type ChartDataMemo = {
views: LineChartData, views: LineChartData;
uploads: LineChartData, uploads: LineChartData;
uploadTypes: ChartData<'pie', number[], string>, uploadTypes: ChartData<'pie', number[], string>;
storage: LineChartData, storage: LineChartData;
} | void; } | void;
export default function Graphs() { export default function Graphs() {
@ -77,41 +87,49 @@ export default function Graphs() {
return { return {
views: { views: {
labels, labels,
datasets: [{ datasets: [
label: 'Views', {
data: viewData, label: 'Views',
borderColor: theme.colors.blue[6], data: viewData,
backgroundColor: theme.colors.blue[0], borderColor: theme.colors.blue[6],
}], backgroundColor: theme.colors.blue[0],
},
],
}, },
uploads: { uploads: {
labels, labels,
datasets: [{ datasets: [
label: 'Uploads', {
data: uploadData, label: 'Uploads',
borderColor: theme.colors.blue[6], data: uploadData,
backgroundColor: theme.colors.blue[0], borderColor: theme.colors.blue[6],
}], backgroundColor: theme.colors.blue[0],
},
],
}, },
uploadTypes: { uploadTypes: {
labels: latest?.data.types_count.map((x) => x.mimetype), labels: latest?.data.types_count.map((x) => x.mimetype),
datasets: [{ datasets: [
data: latest?.data.types_count.map((x) => x.count), {
label: 'Upload Types', data: latest?.data.types_count.map((x) => x.count),
backgroundColor: latest?.data.types_count.map((x) => hash.hex(x.mimetype)), label: 'Upload Types',
}], backgroundColor: latest?.data.types_count.map((x) => hash.hex(x.mimetype)),
},
],
}, },
storage: { storage: {
labels, labels,
datasets: [{ datasets: [
label: 'Storage', {
data: storageData, label: 'Storage',
borderColor: theme.colors.blue[6], data: storageData,
backgroundColor: theme.colors.blue[0], borderColor: theme.colors.blue[6],
}], backgroundColor: theme.colors.blue[0],
},
],
}, },
}; };
}, [historicalStats]); }, [historicalStats]);
@ -125,48 +143,43 @@ export default function Graphs() {
<Grid.Col md={12} lg={4}> <Grid.Col md={12} lg={4}>
<Card> <Card>
<Title size='h4'>Upload Types</Title> <Title size='h4'>Upload Types</Title>
{ {chartData && (
chartData && ( <Pie
<Pie data={chartData.uploadTypes}
data={chartData.uploadTypes} options={{
plugins: {
options={{ datalabels: {
plugins: { formatter: (_, ctx) => {
datalabels: { // mime: count
formatter: (_, ctx) => { const mime = ctx.chart.data.labels[ctx.dataIndex];
// mime: count const count = ctx.chart.data.datasets[0].data[ctx.dataIndex];
const mime = ctx.chart.data.labels[ctx.dataIndex]; return `${mime}: ${count}`;
const count = ctx.chart.data.datasets[0].data[ctx.dataIndex];
return `${mime}: ${count}`;
},
color: 'white',
textShadowBlur: 7,
textShadowColor: 'black',
}, },
color: 'white',
textShadowBlur: 7,
textShadowColor: 'black',
}, },
}} },
style={{ maxHeight: '20vh' }} }}
/> style={{ maxHeight: '20vh' }}
) />
} )}
</Card> </Card>
</Grid.Col> </Grid.Col>
{/* 3/4 - views */} {/* 3/4 - views */}
<Grid.Col md={12} lg={8}> <Grid.Col md={12} lg={8}>
<Card> <Card>
<Title size='h4'>Total Views</Title> <Title size='h4'>Total Views</Title>
{ {chartData && (
chartData && ( <Chart
<Chart type='line'
type='line' data={chartData.views}
data={chartData.views} options={chartOptions}
options={chartOptions} style={{ maxHeight: '20vh' }}
style={{ maxHeight: '20vh' }} />
/> )}
)
}
</Card> </Card>
</Grid.Col> </Grid.Col>
@ -174,16 +187,14 @@ export default function Graphs() {
<Grid.Col md={12} lg={6}> <Grid.Col md={12} lg={6}>
<Card> <Card>
<Title size='h4'>Total Uploads</Title> <Title size='h4'>Total Uploads</Title>
{ {chartData && (
chartData && ( <Chart
<Chart type='line'
type='line' data={chartData.uploads}
data={chartData.uploads} options={chartOptions}
options={chartOptions} style={{ maxHeight: '20vh' }}
style={{ maxHeight: '20vh' }} />
/> )}
)
}
</Card> </Card>
</Grid.Col> </Grid.Col>
@ -191,46 +202,44 @@ export default function Graphs() {
<Grid.Col md={12} lg={6}> <Grid.Col md={12} lg={6}>
<Card> <Card>
<Title size='h4'>Storage Usage</Title> <Title size='h4'>Storage Usage</Title>
{ {chartData && (
chartData && ( <Chart
<Chart type='line'
type='line' data={chartData.storage}
data={chartData.storage} options={{
options={{ ...chartOptions,
...chartOptions,
scales: { scales: {
...chartOptions.scales, ...chartOptions.scales,
y: { y: {
...chartOptions.scales.y, ...chartOptions.scales.y,
ticks: { ticks: {
callback: (value) => bytesToRead(value as number), callback: (value) => bytesToRead(value as number),
color: theme.colors.gray[6], color: theme.colors.gray[6],
},
},
},
plugins: {
...chartOptions.plugins,
tooltip: {
...chartOptions.plugins.tooltip,
callbacks: {
label: (context) => {
const value = context.raw as number;
return bytesToRead(value);
}, },
}, },
}, },
},
plugins: { }}
...chartOptions.plugins, style={{ maxHeight: '20vh' }}
tooltip: { />
...chartOptions.plugins.tooltip, )}
callbacks: {
label: (context) => {
const value = context.raw as number;
return bytesToRead(value);
},
},
},
},
}}
style={{ maxHeight: '20vh' }}
/>
)
}
</Card> </Card>
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</Box> </Box>
); );
} }

View file

@ -4,10 +4,8 @@ import { useStats } from 'lib/queries/stats';
export default function Types() { export default function Types() {
const stats = useStats(); const stats = useStats();
if(stats.isLoading) return ( if (stats.isLoading) return <LoadingOverlay visible />;
<LoadingOverlay visible />
);
const latest = stats.data[0]; const latest = stats.data[0];
@ -35,4 +33,4 @@ export default function Types() {
</Card> </Card>
</Box> </Box>
); );
} }

View file

@ -9,11 +9,10 @@ export default function Stats() {
<Title mb='md'>Stats</Title> <Title mb='md'>Stats</Title>
<StatCards /> <StatCards />
<Types /> <Types />
<Graphs /> <Graphs />
</div> </div>
); );
} }

View file

@ -53,7 +53,7 @@ export default function Upload() {
useEffect(() => { useEffect(() => {
window.addEventListener('paste', (e: ClipboardEvent) => { window.addEventListener('paste', (e: ClipboardEvent) => {
const item = Array.from(e.clipboardData.items).find(x => /^image/.test(x.type)); const item = Array.from(e.clipboardData.items).find((x) => /^image/.test(x.type));
const file = item.getAsFile(); const file = item.getAsFile();
setFiles([...files, file]); setFiles([...files, file]);
showNotification({ showNotification({
@ -64,35 +64,40 @@ export default function Upload() {
}); });
const handleUpload = async () => { const handleUpload = async () => {
const expires_at = expires === 'never' ? null : new Date({ const expires_at =
'5min': Date.now() + 5 * 60 * 1000, expires === 'never'
'10min': Date.now() + 10 * 60 * 1000, ? null
'15min': Date.now() + 15 * 60 * 1000, : new Date(
'30min': Date.now() + 30 * 60 * 1000, {
'1h': Date.now() + 60 * 60 * 1000, '5min': Date.now() + 5 * 60 * 1000,
'2h': Date.now() + 2 * 60 * 60 * 1000, '10min': Date.now() + 10 * 60 * 1000,
'3h': Date.now() + 3 * 60 * 60 * 1000, '15min': Date.now() + 15 * 60 * 1000,
'4h': Date.now() + 4 * 60 * 60 * 1000, '30min': Date.now() + 30 * 60 * 1000,
'5h': Date.now() + 5 * 60 * 60 * 1000, '1h': Date.now() + 60 * 60 * 1000,
'6h': Date.now() + 6 * 60 * 60 * 1000, '2h': Date.now() + 2 * 60 * 60 * 1000,
'8h': Date.now() + 8 * 60 * 60 * 1000, '3h': Date.now() + 3 * 60 * 60 * 1000,
'12h': Date.now() + 12 * 60 * 60 * 1000, '4h': Date.now() + 4 * 60 * 60 * 1000,
'1d': Date.now() + 24 * 60 * 60 * 1000, '5h': Date.now() + 5 * 60 * 60 * 1000,
'3d': Date.now() + 3 * 24 * 60 * 60 * 1000, '6h': Date.now() + 6 * 60 * 60 * 1000,
'5d': Date.now() + 5 * 24 * 60 * 60 * 1000, '8h': Date.now() + 8 * 60 * 60 * 1000,
'7d': Date.now() + 7 * 24 * 60 * 60 * 1000, '12h': Date.now() + 12 * 60 * 60 * 1000,
'1w': Date.now() + 7 * 24 * 60 * 60 * 1000, '1d': Date.now() + 24 * 60 * 60 * 1000,
'1.5w': Date.now() + 1.5 * 7 * 24 * 60 * 60 * 1000, '3d': Date.now() + 3 * 24 * 60 * 60 * 1000,
'2w': Date.now() + 2 * 7 * 24 * 60 * 60 * 1000, '5d': Date.now() + 5 * 24 * 60 * 60 * 1000,
'3w': Date.now() + 3 * 7 * 24 * 60 * 60 * 1000, '7d': Date.now() + 7 * 24 * 60 * 60 * 1000,
'1m': Date.now() + 30 * 24 * 60 * 60 * 1000, '1w': Date.now() + 7 * 24 * 60 * 60 * 1000,
'1.5m': Date.now() + 1.5 * 30 * 24 * 60 * 60 * 1000, '1.5w': Date.now() + 1.5 * 7 * 24 * 60 * 60 * 1000,
'2m': Date.now() + 2 * 30 * 24 * 60 * 60 * 1000, '2w': Date.now() + 2 * 7 * 24 * 60 * 60 * 1000,
'3m': Date.now() + 3 * 30 * 24 * 60 * 60 * 1000, '3w': Date.now() + 3 * 7 * 24 * 60 * 60 * 1000,
'6m': Date.now() + 6 * 30 * 24 * 60 * 60 * 1000, '1m': Date.now() + 30 * 24 * 60 * 60 * 1000,
'8m': Date.now() + 8 * 30 * 24 * 60 * 60 * 1000, '1.5m': Date.now() + 1.5 * 30 * 24 * 60 * 60 * 1000,
'1y': Date.now() + 365 * 24 * 60 * 60 * 1000, '2m': Date.now() + 2 * 30 * 24 * 60 * 60 * 1000,
}[expires]); '3m': Date.now() + 3 * 30 * 24 * 60 * 60 * 1000,
'6m': Date.now() + 6 * 30 * 24 * 60 * 60 * 1000,
'8m': Date.now() + 8 * 30 * 24 * 60 * 60 * 1000,
'1y': Date.now() + 365 * 24 * 60 * 60 * 1000,
}[expires]
);
setProgress(0); setProgress(0);
setLoading(true); setLoading(true);
@ -108,39 +113,53 @@ export default function Upload() {
}); });
const req = new XMLHttpRequest(); const req = new XMLHttpRequest();
req.upload.addEventListener('progress', e => { req.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) { if (e.lengthComputable) {
setProgress(Math.round(e.loaded / e.total * 100)); setProgress(Math.round((e.loaded / e.total) * 100));
} }
}); });
req.addEventListener('load', e => { req.addEventListener(
// @ts-ignore not sure why it thinks response doesnt exist, but it does. 'load',
const json = JSON.parse(e.target.response); (e) => {
setLoading(false); // @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) { if (json.error === undefined) {
updateNotification({ updateNotification({
id: 'upload', id: 'upload',
title: 'Upload Successful', title: 'Upload Successful',
message: <>Copied first file to clipboard! <br />{json.files.map(x => (<Link key={x} href={x}>{x}<br /></Link>))}</>, message: (
color: 'green', <>
icon: <UploadIcon />, Copied first file to clipboard! <br />
}); {json.files.map((x) => (
clipboard.copy(json.files[0]); <Link key={x} href={x}>
setFiles([]); {x}
invalidateFiles(); <br />
} else { </Link>
updateNotification({ ))}
id: 'upload', </>
title: 'Upload Failed', ),
message: json.error, color: 'green',
color: 'red', icon: <UploadIcon />,
icon: <CrossIcon />, });
}); clipboard.copy(json.files[0]);
} setFiles([]);
setProgress(0); invalidateFiles();
}, false); } else {
updateNotification({
id: 'upload',
title: 'Upload Failed',
message: json.error,
color: 'red',
icon: <CrossIcon />,
});
}
setProgress(0);
},
false
);
req.open('POST', '/api/upload'); req.open('POST', '/api/upload');
req.setRequestHeader('Authorization', user.token); req.setRequestHeader('Authorization', user.token);
@ -156,7 +175,9 @@ export default function Upload() {
<Dropzone loading={loading} onDrop={(f) => setFiles([...files, ...f])}> <Dropzone loading={loading} onDrop={(f) => setFiles([...files, ...f])}>
<Group position='center' spacing='md'> <Group position='center' spacing='md'>
{files.map(file => (<FileDropzone key={randomId()} file={file} />))} {files.map((file) => (
<FileDropzone key={randomId()} file={file} />
))}
</Group> </Group>
</Dropzone> </Dropzone>
@ -167,7 +188,7 @@ export default function Upload() {
<Group position='right' mt='md'> <Group position='right' mt='md'>
<Tooltip label='Add a password to your files (optional, leave blank for none)'> <Tooltip label='Add a password to your files (optional, leave blank for none)'>
<PasswordInput <PasswordInput
style={{width: '252px'}} style={{ width: '252px' }}
placeholder='Password' placeholder='Password'
value={password} value={password}
onChange={(e) => setPassword(e.currentTarget.value)} onChange={(e) => setPassword(e.currentTarget.value)}
@ -210,7 +231,9 @@ export default function Upload() {
]} ]}
/> />
</Tooltip> </Tooltip>
<Button leftIcon={<UploadIcon />} onClick={handleUpload} disabled={files.length === 0 ? true : false}>Upload</Button> <Button leftIcon={<UploadIcon />} onClick={handleUpload} disabled={files.length === 0 ? true : false}>
Upload
</Button>
</Group> </Group>
</> </>
); );

View file

@ -26,7 +26,7 @@ export default function Upload() {
}); });
const req = new XMLHttpRequest(); const req = new XMLHttpRequest();
req.addEventListener('load', e => { req.addEventListener('load', (e) => {
// @ts-ignore not sure why it thinks response doesnt exist, but it does. // @ts-ignore not sure why it thinks response doesnt exist, but it does.
const json = JSON.parse(e.target.response); const json = JSON.parse(e.target.response);
@ -34,7 +34,17 @@ export default function Upload() {
updateNotification({ updateNotification({
id: 'upload-text', id: 'upload-text',
title: 'Upload Successful', title: 'Upload Successful',
message: <>Copied first file to clipboard! <br />{json.files.map(x => (<Link key={x} href={x}>{x}<br /></Link>))}</>, message: (
<>
Copied first file to clipboard! <br />
{json.files.map((x) => (
<Link key={x} href={x}>
{x}
<br />
</Link>
))}
</>
),
}); });
} }
}); });
@ -53,20 +63,23 @@ export default function Upload() {
<> <>
<Title mb='md'>Upload Text</Title> <Title mb='md'>Upload Text</Title>
<CodeInput <CodeInput value={value} onChange={(e) => setValue(e.target.value)} />
value={value}
onChange={e => setValue(e.target.value)}
/>
<Group position='right' mt='md'> <Group position='right' mt='md'>
<Select <Select
value={lang} value={lang}
onChange={setLang} onChange={setLang}
dropdownPosition='top' dropdownPosition='top'
data={Object.keys(exts).map(x => ({ value: x, label: exts[x] }))} data={Object.keys(exts).map((x) => ({ value: x, label: exts[x] }))}
icon={<TypeIcon />} icon={<TypeIcon />}
/> />
<Button leftIcon={<UploadIcon />} onClick={handleUpload} disabled={value.trim().length === 0 ? true : false}>Upload</Button> <Button
leftIcon={<UploadIcon />}
onClick={handleUpload}
disabled={value.trim().length === 0 ? true : false}
>
Upload
</Button>
</Group> </Group>
</> </>
); );

View file

@ -5,13 +5,11 @@ import { CopyIcon, CrossIcon, DeleteIcon, ExternalLinkIcon } from 'components/ic
import TrashIcon from 'components/icons/TrashIcon'; import TrashIcon from 'components/icons/TrashIcon';
import { URLResponse, useURLDelete } from 'lib/queries/url'; import { URLResponse, useURLDelete } from 'lib/queries/url';
export default function URLCard({ url }: { export default function URLCard({ url }: { url: URLResponse }) {
url: URLResponse
}) {
const clipboard = useClipboard(); const clipboard = useClipboard();
const urlDelete = useURLDelete(); const urlDelete = useURLDelete();
const copyURL = u => { const copyURL = (u) => {
clipboard.copy(`${window.location.protocol}//${window.location.host}${u.url}`); clipboard.copy(`${window.location.protocol}//${window.location.host}${u.url}`);
showNotification({ showNotification({
title: 'Copied to clipboard', title: 'Copied to clipboard',
@ -20,7 +18,7 @@ export default function URLCard({ url }: {
}); });
}; };
const deleteURL = async u => { const deleteURL = async (u) => {
urlDelete.mutate(u.id, { urlDelete.mutate(u.id, {
onSuccess: () => { onSuccess: () => {
showNotification({ showNotification({
@ -44,7 +42,7 @@ export default function URLCard({ url }: {
return ( return (
<Card key={url.id} sx={{ maxWidth: '100%' }} shadow='sm'> <Card key={url.id} sx={{ maxWidth: '100%' }} shadow='sm'>
<LoadingOverlay visible={urlDelete.isLoading}/> <LoadingOverlay visible={urlDelete.isLoading} />
<Group position='apart'> <Group position='apart'>
<Group position='left'> <Group position='left'>
@ -64,4 +62,4 @@ export default function URLCard({ url }: {
</Group> </Group>
</Card> </Card>
); );
} }

View file

@ -1,4 +1,15 @@
import { ActionIcon, Button, Group, Modal, SimpleGrid, Skeleton, TextInput, Title, Card, Center } from '@mantine/core'; import {
ActionIcon,
Button,
Group,
Modal,
SimpleGrid,
Skeleton,
TextInput,
Title,
Card,
Center,
} from '@mantine/core';
import { useForm } from '@mantine/form'; import { useForm } from '@mantine/form';
import { showNotification } from '@mantine/notifications'; import { showNotification } from '@mantine/notifications';
import { CrossIcon, LinkIcon, PlusIcon } from 'components/icons'; import { CrossIcon, LinkIcon, PlusIcon } from 'components/icons';
@ -24,11 +35,11 @@ export default function Urls() {
}, },
}); });
const onSubmit = async values => { const onSubmit = async (values) => {
const cleanURL = values.url.trim(); const cleanURL = values.url.trim();
const cleanVanity = values.vanity.trim(); const cleanVanity = values.vanity.trim();
if (cleanURL === '') return form.setFieldError('url', 'URL can\'t be nothing'); if (cleanURL === '') return form.setFieldError('url', "URL can't be nothing");
try { try {
new URL(cleanURL); new URL(cleanURL);
@ -45,7 +56,7 @@ export default function Urls() {
const res = await fetch('/api/shorten', { const res = await fetch('/api/shorten', {
method: 'POST', method: 'POST',
headers: { headers: {
'Authorization': user.token, Authorization: user.token,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data), body: JSON.stringify(data),
@ -77,11 +88,7 @@ export default function Urls() {
return ( return (
<> <>
<Modal <Modal opened={createOpen} onClose={() => setCreateOpen(false)} title={<Title>Shorten URL</Title>}>
opened={createOpen}
onClose={() => setCreateOpen(false)}
title={<Title>Shorten URL</Title>}
>
<form onSubmit={form.onSubmit((v) => onSubmit(v))}> <form onSubmit={form.onSubmit((v) => onSubmit(v))}>
<TextInput id='url' label='URL' {...form.getInputProps('url')} /> <TextInput id='url' label='URL' {...form.getInputProps('url')} />
<TextInput id='vanity' label='Vanity' {...form.getInputProps('vanity')} /> <TextInput id='vanity' label='Vanity' {...form.getInputProps('vanity')} />
@ -95,44 +102,32 @@ export default function Urls() {
<Group mb='md'> <Group mb='md'>
<Title>URLs</Title> <Title>URLs</Title>
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}><PlusIcon /></ActionIcon> <ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}>
<PlusIcon />
</ActionIcon>
</Group> </Group>
{ {urls.data && urls.data.length === 0 && (
(urls.data && urls.data.length === 0) && ( <Card shadow='md'>
<Card shadow='md'> <Center>
<Center> <Group>
<Group> <div>
<div> <LinkIcon size={48} />
<LinkIcon size={48} /> </div>
</div> <div>
<div> <Title>Nothing here</Title>
<Title>Nothing here</Title> <MutedText size='md'>Create a link to get started!</MutedText>
<MutedText size='md'>Create a link to get started!</MutedText> </div>
</div> </Group>
</Group> </Center>
</Center> </Card>
</Card> )}
)
}
<SimpleGrid <SimpleGrid cols={4} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
cols={4} {urls.isLoading || !urls.data
spacing='lg' ? [1, 2, 3, 4].map((x) => <Skeleton key={x} width='100%' height={80} radius='sm' />)
breakpoints={[ : urls.data.map((url) => <URLCard key={url.id} url={url} />)}
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
]}
>
{
(urls.isLoading || !urls.data) ?
[1, 2, 3, 4].map(x => (
<Skeleton key={x} width='100%' height={80} radius='sm' />
))
: urls.data.map(url => (
<URLCard key={url.id} url={url} />
))
}
</SimpleGrid> </SimpleGrid>
</> </>
); );
} }

View file

@ -13,11 +13,11 @@ export function CreateUserModal({ open, setOpen, updateUsers }) {
}, },
}); });
const onSubmit = async values => { const onSubmit = async (values) => {
const cleanUsername = values.username.trim(); const cleanUsername = values.username.trim();
const cleanPassword = values.password.trim(); const cleanPassword = values.password.trim();
if (cleanUsername === '') return form.setFieldError('username', 'Username can\'t be nothing'); if (cleanUsername === '') return form.setFieldError('username', "Username can't be nothing");
if (cleanPassword === '') return form.setFieldError('password', 'Password can\'t be nothing'); if (cleanPassword === '') return form.setFieldError('password', "Password can't be nothing");
const data = { const data = {
username: cleanUsername, username: cleanUsername,
@ -47,12 +47,8 @@ export function CreateUserModal({ open, setOpen, updateUsers }) {
}; };
return ( return (
<Modal <Modal opened={open} onClose={() => setOpen(false)} title={<Title>Create User</Title>}>
opened={open} <form onSubmit={form.onSubmit((v) => onSubmit(v))}>
onClose={() => setOpen(false)}
title={<Title>Create User</Title>}
>
<form onSubmit={form.onSubmit(v => onSubmit(v))}>
<TextInput id='username' label='Username' {...form.getInputProps('username')} /> <TextInput id='username' label='Username' {...form.getInputProps('username')} />
<TextInput id='password' label='Password' type='password' {...form.getInputProps('password')} /> <TextInput id='password' label='Password' type='password' {...form.getInputProps('password')} />
<Switch mt={12} id='administrator' label='Administrator' {...form.getInputProps('administrator')} /> <Switch mt={12} id='administrator' label='Administrator' {...form.getInputProps('administrator')} />
@ -64,4 +60,4 @@ export function CreateUserModal({ open, setOpen, updateUsers }) {
</form> </form>
</Modal> </Modal>
); );
} }

View file

@ -6,16 +6,17 @@ import useFetch from 'hooks/useFetch';
export function EditUserModal({ open, setOpen, updateUsers, user }) { export function EditUserModal({ open, setOpen, updateUsers, user }) {
let form; let form;
if (user) form = useForm({
initialValues: {
username: user?.username,
password: '',
administrator: user?.administrator,
},
});
const onSubmit = async values => { if (user)
form = useForm({
initialValues: {
username: user?.username,
password: '',
administrator: user?.administrator,
},
});
const onSubmit = async (values) => {
const cleanUsername = values.username.trim(); const cleanUsername = values.username.trim();
const cleanPassword = values.password.trim(); const cleanPassword = values.password.trim();
@ -28,7 +29,6 @@ export function EditUserModal({ open, setOpen, updateUsers, user }) {
if (cleanUsername !== '' && cleanUsername !== user.username) data.username = cleanUsername; if (cleanUsername !== '' && cleanUsername !== user.username) data.username = cleanUsername;
if (cleanPassword !== '') data.password = cleanPassword; if (cleanPassword !== '') data.password = cleanPassword;
setOpen(false); setOpen(false);
const res = await useFetch('/api/user/' + user.id, 'PATCH', data); const res = await useFetch('/api/user/' + user.id, 'PATCH', data);
if (res.error) { if (res.error) {
@ -51,13 +51,9 @@ export function EditUserModal({ open, setOpen, updateUsers, user }) {
}; };
return ( return (
<Modal <Modal opened={open} onClose={() => setOpen(false)} title={<Title>Edit User {user?.username}</Title>}>
opened={open}
onClose={() => setOpen(false)}
title={<Title>Edit User {user?.username}</Title>}
>
{user && ( {user && (
<form onSubmit={form.onSubmit(v => onSubmit(v))}> <form onSubmit={form.onSubmit((v) => onSubmit(v))}>
<TextInput id='username' label='Username' {...form.getInputProps('username')} /> <TextInput id='username' label='Username' {...form.getInputProps('username')} />
<TextInput id='password' label='Password' type='password' {...form.getInputProps('password')} /> <TextInput id='password' label='Password' type='password' {...form.getInputProps('password')} />
<Switch mt={12} id='administrator' label='Administrator' {...form.getInputProps('administrator')} /> <Switch mt={12} id='administrator' label='Administrator' {...form.getInputProps('administrator')} />

View file

@ -45,27 +45,28 @@ export default function Users() {
}; };
// 2-step modal for deleting user if they want to delete their images too. // 2-step modal for deleting user if they want to delete their images too.
const openDeleteModal = user => modals.openConfirmModal({ const openDeleteModal = (user) =>
title: `Delete ${user.username}?`, modals.openConfirmModal({
closeOnConfirm: false, title: `Delete ${user.username}?`,
labels: { confirm: 'Yes', cancel: 'No' }, closeOnConfirm: false,
onConfirm: () => { labels: { confirm: 'Yes', cancel: 'No' },
modals.openConfirmModal({ onConfirm: () => {
title: `Delete ${user.username}'s images?`, modals.openConfirmModal({
labels: { confirm: 'Yes', cancel: 'No' }, title: `Delete ${user.username}'s images?`,
centered: true, labels: { confirm: 'Yes', cancel: 'No' },
overlayBlur: 3, centered: true,
onConfirm: () => { overlayBlur: 3,
handleDelete(user, true); onConfirm: () => {
modals.closeAll(); handleDelete(user, true);
}, modals.closeAll();
onCancel: () => { },
handleDelete(user, false); onCancel: () => {
modals.closeAll(); handleDelete(user, false);
}, modals.closeAll();
}); },
}, });
}); },
});
const updateUsers = async () => { const updateUsers = async () => {
const us = await useFetch('/api/users'); const us = await useFetch('/api/users');
@ -73,7 +74,7 @@ export default function Users() {
setUsers(us); setUsers(us);
} else { } else {
router.push('/dashboard'); router.push('/dashboard');
}; }
}; };
useEffect(() => { useEffect(() => {
@ -84,48 +85,57 @@ export default function Users() {
<> <>
<CreateUserModal open={createOpen} setOpen={setCreateOpen} updateUsers={updateUsers} /> <CreateUserModal open={createOpen} setOpen={setCreateOpen} updateUsers={updateUsers} />
<EditUserModal open={editOpen} setOpen={setEditOpen} updateUsers={updateUsers} user={selectedUser} /> <EditUserModal open={editOpen} setOpen={setEditOpen} updateUsers={updateUsers} user={selectedUser} />
<Group mb='md'> <Group mb='md'>
<Title>Users</Title> <Title>Users</Title>
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}><PlusIcon /></ActionIcon> <ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}>
<PlusIcon />
</ActionIcon>
</Group> </Group>
<SimpleGrid <SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
cols={3} {users.length
spacing='lg' ? users
breakpoints={[ .filter((x) => x.username !== user.username)
{ maxWidth: 'sm', cols: 1, spacing: 'sm' }, .map((user) => (
]} <Card key={user.id} sx={{ maxWidth: '100%' }}>
> <Group position='apart'>
{users.length ? users.filter(x => x.username !== user.username).map(user => ( <Group position='left'>
<Card key={user.id} sx={{ maxWidth: '100%' }}> <Avatar
<Group position='apart'> size='lg'
<Group position='left'> color={user.administrator ? 'primary' : 'dark'}
<Avatar size='lg' color={user.administrator ? 'primary' : 'dark'} src={user.avatar ?? null}>{user.username[0]}</Avatar> src={user.avatar ?? null}
<Stack spacing={0}> >
<Title>{user.username}</Title> {user.username[0]}
<MutedText size='sm'>ID: {user.id}</MutedText> </Avatar>
<MutedText size='sm'>Administrator: {user.administrator ? 'yes' : 'no'}</MutedText> <Stack spacing={0}>
</Stack> <Title>{user.username}</Title>
</Group> <MutedText size='sm'>ID: {user.id}</MutedText>
<Group position='right'> <MutedText size='sm'>Administrator: {user.administrator ? 'yes' : 'no'}</MutedText>
{user.administrator ? null : ( </Stack>
<> </Group>
<ActionIcon aria-label='edit' onClick={() => {setEditOpen(true); setSelectedUser(user);}}> <Group position='right'>
<PencilIcon /> {user.administrator ? null : (
</ActionIcon> <>
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(user)}> <ActionIcon
<DeleteIcon /> aria-label='edit'
</ActionIcon> onClick={() => {
</> setEditOpen(true);
)} setSelectedUser(user);
}}
</Group> >
</Group> <PencilIcon />
</Card> </ActionIcon>
)) : [1, 2, 3].map(x => ( <ActionIcon aria-label='delete' onClick={() => openDeleteModal(user)}>
<Skeleton key={x} width='100%' height={100} radius='sm' /> <DeleteIcon />
))} </ActionIcon>
</>
)}
</Group>
</Group>
</Card>
))
: [1, 2, 3].map((x) => <Skeleton key={x} width='100%' height={100} radius='sm' />)}
</SimpleGrid> </SimpleGrid>
</> </>
); );
} }

View file

@ -4,4 +4,4 @@ import validateConfig from './config/validateConfig';
if (!global.config) global.config = validateConfig(readConfig()); if (!global.config) global.config = validateConfig(readConfig());
export default global.config as Config; export default global.config as Config;

View file

@ -5,7 +5,7 @@ export interface ConfigCore {
port: number; port: number;
database_url: string; database_url: string;
logger: boolean; logger: boolean;
stats_interval: number; stats_interval: number;
invites_interval: number; invites_interval: number;
} }
@ -121,4 +121,4 @@ export interface Config {
discord: ConfigDiscord; discord: ConfigDiscord;
oauth: ConfigOAuth; oauth: ConfigOAuth;
features: ConfigFeatures; features: ConfigFeatures;
} }

View file

@ -63,7 +63,6 @@ export default function readConfig() {
map('DATASOURCE_S3_REGION', 'string', 'datasource.s3.region'), map('DATASOURCE_S3_REGION', 'string', 'datasource.s3.region'),
map('DATASOURCE_S3_USE_SSL', 'boolean', 'datasource.s3.use_ssl'), map('DATASOURCE_S3_USE_SSL', 'boolean', 'datasource.s3.use_ssl'),
map('DATASOURCE_SWIFT_USERNAME', 'string', 'datasource.swift.username'), map('DATASOURCE_SWIFT_USERNAME', 'string', 'datasource.swift.username'),
map('DATASOURCE_SWIFT_PASSWORD', 'string', 'datasource.swift.password'), map('DATASOURCE_SWIFT_PASSWORD', 'string', 'datasource.swift.password'),
map('DATASOURCE_SWIFT_AUTH_ENDPOINT', 'string', 'datasource.swift.auth_endpoint'), map('DATASOURCE_SWIFT_AUTH_ENDPOINT', 'string', 'datasource.swift.auth_endpoint'),
@ -152,10 +151,10 @@ export default function readConfig() {
default: default:
parsed = value; parsed = value;
break; break;
}; }
set(config, map.path, parsed); set(config, map.path, parsed);
} }
} }
return config; return config;
} }

View file

@ -3,18 +3,22 @@ import { CombinedError, s, ValidationError } from '@sapphire/shapeshift';
import { inspect } from 'util'; import { inspect } from 'util';
import Logger from 'lib/logger'; import Logger from 'lib/logger';
const discord_content = s.object({ const discord_content = s
content: s.string.nullish.default(null), .object({
embed: s.object({ content: s.string.nullish.default(null),
title: s.string.nullish.default(null), embed: s
description: s.string.nullish.default(null), .object({
footer: s.string.nullish.default(null), title: s.string.nullish.default(null),
color: s.number.notEqual(NaN).nullish.default(null), description: s.string.nullish.default(null),
thumbnail: s.boolean.default(false), footer: s.string.nullish.default(null),
image: s.boolean.default(true), color: s.number.notEqual(NaN).nullish.default(null),
timestamp: s.boolean.default(true), thumbnail: s.boolean.default(false),
}).default(null), image: s.boolean.default(true),
}).default(null); timestamp: s.boolean.default(true),
})
.default(null),
})
.default(null);
const validator = s.object({ const validator = s.object({
core: s.object({ core: s.object({
@ -27,117 +31,139 @@ const validator = s.object({
stats_interval: s.number.default(1800), stats_interval: s.number.default(1800),
invites_interval: s.number.default(1800), invites_interval: s.number.default(1800),
}), }),
datasource: s.object({ datasource: s
type: s.enum('local', 's3', 'swift').default('local'), .object({
local: s.object({ type: s.enum('local', 's3', 'swift').default('local'),
directory: s.string.default('./uploads'), local: s
}).default({ .object({
directory: './uploads', directory: s.string.default('./uploads'),
})
.default({
directory: './uploads',
}),
s3: s.object({
access_key_id: s.string,
secret_access_key: s.string,
endpoint: s.string,
bucket: s.string,
force_s3_path: s.boolean.default(false),
region: s.string.default('us-east-1'),
use_ssl: s.boolean.default(false),
}).optional,
swift: s.object({
username: s.string,
password: s.string,
auth_endpoint: s.string,
container: s.string,
project_id: s.string,
domain_id: s.string.default('default'),
region_id: s.string.nullable,
}).optional,
})
.default({
type: 'local',
local: {
directory: './uploads',
},
s3: {
region: 'us-east-1',
force_s3_path: false,
},
swift: {
domain_id: 'default',
},
}), }),
s3: s.object({ uploader: s
access_key_id: s.string, .object({
secret_access_key: s.string, route: s.string.default('/u'),
endpoint: s.string, embed_route: s.string.default('/a'),
bucket: s.string, length: s.number.default(6),
force_s3_path: s.boolean.default(false), admin_limit: s.number.default(104900000),
region: s.string.default('us-east-1'), user_limit: s.number.default(104900000),
use_ssl: s.boolean.default(false), disabled_extensions: s.string.array.default([]),
}).optional, format_date: s.string.default('YYYY-MM-DD_HH:mm:ss'),
swift: s.object({ })
username: s.string, .default({
password: s.string, route: '/u',
auth_endpoint: s.string, embed_route: '/a',
container: s.string, length: 6,
project_id: s.string, admin_limit: 104900000,
domain_id: s.string.default('default'), user_limit: 104900000,
region_id: s.string.nullable, disabled_extensions: [],
}).optional, format_date: 'YYYY-MM-DD_HH:mm:ss',
}).default({ }),
type: 'local', urls: s
local: { .object({
directory: './uploads', route: s.string.default('/go'),
}, length: s.number.default(6),
s3: { })
region: 'us-east-1', .default({
force_s3_path: false, route: '/go',
}, length: 6,
swift: { }),
domain_id: 'default', ratelimit: s
}, .object({
}), user: s.number.default(0),
uploader: s.object({ admin: s.number.default(0),
route: s.string.default('/u'), })
embed_route: s.string.default('/a'), .default({
length: s.number.default(6), user: 0,
admin_limit: s.number.default(104900000), admin: 0,
user_limit: s.number.default(104900000), }),
disabled_extensions: s.string.array.default([]), website: s
format_date: s.string.default('YYYY-MM-DD_HH:mm:ss'), .object({
}).default({ title: s.string.default('Zipline'),
route: '/u', show_files_per_user: s.boolean.default(true),
embed_route: '/a', show_version: s.boolean.default(true),
length: 6, disable_media_preview: s.boolean.default(false),
admin_limit: 104900000,
user_limit: 104900000,
disabled_extensions: [],
format_date: 'YYYY-MM-DD_HH:mm:ss',
}),
urls: s.object({
route: s.string.default('/go'),
length: s.number.default(6),
}).default({
route: '/go',
length: 6,
}),
ratelimit: s.object({
user: s.number.default(0),
admin: s.number.default(0),
}).default({
user: 0,
admin: 0,
}),
website: s.object({
title: s.string.default('Zipline'),
show_files_per_user: s.boolean.default(true),
show_version: s.boolean.default(true),
disable_media_preview: s.boolean.default(false),
external_links: s.array(s.object({ external_links: s
label: s.string, .array(
link: s.string, s.object({
})).default([ label: s.string,
{ label: 'Zipline', link: 'https://github.com/diced/zipline' }, link: s.string,
{ label: 'Documentation', link: 'https://zipline.diced.tech/' }, })
]), )
}).default({ .default([
title: 'Zipline', { label: 'Zipline', link: 'https://github.com/diced/zipline' },
show_files_per_user: true, { label: 'Documentation', link: 'https://zipline.diced.tech/' },
show_version: true, ]),
disable_media_preview: false, })
.default({
title: 'Zipline',
show_files_per_user: true,
show_version: true,
disable_media_preview: false,
external_links: [ external_links: [
{ label: 'Zipline', link: 'https://github.com/diced/zipline' }, { label: 'Zipline', link: 'https://github.com/diced/zipline' },
{ label: 'Documentation', link: 'https://zipline.diced.tech/' }, { label: 'Documentation', link: 'https://zipline.diced.tech/' },
], ],
}), }),
discord: s.object({ discord: s.object({
url: s.string, url: s.string,
username: s.string.default('Zipline'), username: s.string.default('Zipline'),
avatar_url: s.string.default('https://raw.githubusercontent.com/diced/zipline/9b60147e112ec5b70170500b85c75ea621f41d03/public/zipline.png'), avatar_url: s.string.default(
'https://raw.githubusercontent.com/diced/zipline/9b60147e112ec5b70170500b85c75ea621f41d03/public/zipline.png'
),
upload: discord_content, upload: discord_content,
shorten: discord_content, shorten: discord_content,
}), }),
oauth: s.object({ oauth: s
github_client_id: s.string.nullable.default(null), .object({
github_client_secret: s.string.nullable.default(null), github_client_id: s.string.nullable.default(null),
github_client_secret: s.string.nullable.default(null),
discord_client_id: s.string.nullable.default(null), discord_client_id: s.string.nullable.default(null),
discord_client_secret: s.string.nullable.default(null), discord_client_secret: s.string.nullable.default(null),
}).nullish.default(null), })
features: s.object({ .nullish.default(null),
invites: s.boolean.default(true), features: s
oauth_registration: s.boolean.default(false), .object({
}).default({ invites: true, oauth_registration: false }), invites: s.boolean.default(true),
oauth_registration: s.boolean.default(false),
})
.default({ invites: true, oauth_registration: false }),
}); });
export default function validate(config): Config { export default function validate(config): Config {
@ -150,10 +176,8 @@ export default function validate(config): Config {
errors.push('datasource.s3.access_key_id is a required field'); errors.push('datasource.s3.access_key_id is a required field');
if (!validated.datasource.s3.secret_access_key) if (!validated.datasource.s3.secret_access_key)
errors.push('datasource.s3.secret_access_key is a required field'); errors.push('datasource.s3.secret_access_key is a required field');
if (!validated.datasource.s3.bucket) if (!validated.datasource.s3.bucket) errors.push('datasource.s3.bucket is a required field');
errors.push('datasource.s3.bucket is a required field'); if (!validated.datasource.s3.endpoint) errors.push('datasource.s3.endpoint is a required field');
if (!validated.datasource.s3.endpoint)
errors.push('datasource.s3.endpoint is a required field');
if (errors.length) throw { errors }; if (errors.length) throw { errors };
break; break;
} }
@ -185,4 +209,4 @@ export default function validate(config): Config {
process.exit(1); process.exit(1);
} }
} }

View file

@ -21,4 +21,4 @@ if (!global.datasource) {
} }
} }
export default global.datasource; export default global.datasource;

View file

@ -8,4 +8,4 @@ export abstract class Datasource {
public abstract size(file: string): Promise<number>; public abstract size(file: string): Promise<number>;
public abstract get(file: string): Readable | Promise<Readable>; public abstract get(file: string): Readable | Promise<Readable>;
public abstract fullSize(): Promise<number>; public abstract fullSize(): Promise<number>;
} }

View file

@ -31,19 +31,19 @@ export class Local extends Datasource {
public async size(file: string): Promise<number> { public async size(file: string): Promise<number> {
const stats = await stat(join(process.cwd(), this.path, file)); const stats = await stat(join(process.cwd(), this.path, file));
return stats.size; return stats.size;
} }
public async fullSize(): Promise<number> { public async fullSize(): Promise<number> {
const files = await readdir(this.path); const files = await readdir(this.path);
let size = 0; let size = 0;
for (let i = 0, L = files.length; i !== L; ++i) { for (let i = 0, L = files.length; i !== L; ++i) {
const sta = await stat(join(this.path, files[i])); const sta = await stat(join(this.path, files[i]));
size += sta.size; size += sta.size;
} }
return size; return size;
} }
} }

View file

@ -7,9 +7,7 @@ export class S3 extends Datasource {
public name: string = 'S3'; public name: string = 'S3';
public s3: Client; public s3: Client;
public constructor( public constructor(public config: ConfigS3Datasource) {
public config: ConfigS3Datasource,
) {
super(); super();
this.s3 = new Client({ this.s3 = new Client({
endPoint: config.endpoint, endPoint: config.endpoint,
@ -55,11 +53,11 @@ export class S3 extends Datasource {
const objects = this.s3.listObjectsV2(this.config.bucket, '', true); const objects = this.s3.listObjectsV2(this.config.bucket, '', true);
let size = 0; let size = 0;
objects.on('data', item => size += item.size); objects.on('data', (item) => (size += item.size));
objects.on('end', err => { objects.on('end', (err) => {
if (err) rej(err); if (err) rej(err);
else res(size); else res(size);
}); });
}); });
} }
} }

View file

@ -44,9 +44,7 @@ class SwiftContainer {
const endpoint = catalogEntry.endpoints.find( const endpoint = catalogEntry.endpoints.find(
(x: any) => (x: any) =>
x.interface === (this.options.credentials.interface || 'public') && x.interface === (this.options.credentials.interface || 'public') &&
(this.options.credentials.region_id (this.options.credentials.region_id ? x.region_id == this.options.credentials.region_id : true)
? x.region_id == this.options.credentials.region_id
: true)
); );
return endpoint ? endpoint.url : null; return endpoint ? endpoint.url : null;
@ -94,7 +92,8 @@ class SwiftContainer {
} }
}); });
if (error || !json || !headers || json.error) throw new Error('Could not retrieve credentials from OpenStack, check your config file'); if (error || !json || !headers || json.error)
throw new Error('Could not retrieve credentials from OpenStack, check your config file');
const catalog = json.token.catalog; const catalog = json.token.catalog;
// many Swift clouds use ceph radosgw to provide swift // many Swift clouds use ceph radosgw to provide swift
@ -124,10 +123,15 @@ class SwiftContainer {
public async listObjects(query?: string): Promise<SwiftObject[]> { public async listObjects(query?: string): Promise<SwiftObject[]> {
const auth = await this.authenticate(); const auth = await this.authenticate();
return await fetch(`${auth.swiftURL}/${this.options.credentials.container}${query ? `${query.startsWith('?') ? '' : '?'}${query}` : ''}`, { return await fetch(
method: 'GET', `${auth.swiftURL}/${this.options.credentials.container}${
headers: this.generateHeaders(auth.token), query ? `${query.startsWith('?') ? '' : '?'}${query}` : ''
}).then((e) => e.json()); }`,
{
method: 'GET',
headers: this.generateHeaders(auth.token),
}
).then((e) => e.json());
} }
public async uploadObject(name: string, data: Buffer): Promise<any> { public async uploadObject(name: string, data: Buffer): Promise<any> {
@ -216,7 +220,7 @@ export class Swift extends Datasource {
public async size(file: string): Promise<number> { public async size(file: string): Promise<number> {
try { try {
const head = await this.container.headObject(file); const head = await this.container.headObject(file);
return head.headers.get('content-length') || 0; return head.headers.get('content-length') || 0;
} catch { } catch {
return 0; return 0;

View file

@ -1,4 +1,4 @@
export { Datasource } from './Datasource'; export { Datasource } from './Datasource';
export { Local } from './Local'; export { Local } from './Local';
export { S3 } from './S3'; export { S3 } from './S3';
export { Swift } from './Swift'; export { Swift } from './Swift';

View file

@ -15,37 +15,44 @@ function parse(str: string, args: Args) {
.replace(/{user\.name}/gi, args[0].username) .replace(/{user\.name}/gi, args[0].username)
.replace(/{link}/gi, args[3]); .replace(/{link}/gi, args[3]);
if (args[1]) str = str if (args[1])
.replace(/{file\.id}/gi, args[1].id.toString()) str = str
.replace(/{file\.mime}/gi, args[1].mimetype) .replace(/{file\.id}/gi, args[1].id.toString())
.replace(/{file\.file}/gi, args[1].file) .replace(/{file\.mime}/gi, args[1].mimetype)
.replace(/{file\.created_at.full_string}/gi, args[1].created_at.toLocaleString()) .replace(/{file\.file}/gi, args[1].file)
.replace(/{file\.created_at.time_string}/gi, args[1].created_at.toLocaleTimeString()) .replace(/{file\.created_at.full_string}/gi, args[1].created_at.toLocaleString())
.replace(/{file\.created_at.date_string}/gi, args[1].created_at.toLocaleDateString()); .replace(/{file\.created_at.time_string}/gi, args[1].created_at.toLocaleTimeString())
.replace(/{file\.created_at.date_string}/gi, args[1].created_at.toLocaleDateString());
if (args[2]) str = str if (args[2])
.replace(/{url\.id}/gi, args[2].id.toString()) str = str
.replace(/{url\.vanity}/gi, args[2].vanity ? args[2].vanity : 'none') .replace(/{url\.id}/gi, args[2].id.toString())
.replace(/{url\.destination}/gi, args[2].destination) .replace(/{url\.vanity}/gi, args[2].vanity ? args[2].vanity : 'none')
.replace(/{url\.created_at.full_string}/gi, args[2].created_at.toLocaleString()) .replace(/{url\.destination}/gi, args[2].destination)
.replace(/{url\.created_at.time_string}/gi, args[2].created_at.toLocaleTimeString()) .replace(/{url\.created_at.full_string}/gi, args[2].created_at.toLocaleString())
.replace(/{url\.created_at.date_string}/gi, args[2].created_at.toLocaleDateString()); .replace(/{url\.created_at.time_string}/gi, args[2].created_at.toLocaleTimeString())
.replace(/{url\.created_at.date_string}/gi, args[2].created_at.toLocaleDateString());
return str; return str;
} }
export function parseContent(content: ConfigDiscordContent, args: Args): ConfigDiscordContent & { url: string } { export function parseContent(
content: ConfigDiscordContent,
args: Args
): ConfigDiscordContent & { url: string } {
return { return {
content: parse(content.content, args), content: parse(content.content, args),
embed: content.embed ? { embed: content.embed
title: parse(content.embed.title, args), ? {
description: parse(content.embed.description, args), title: parse(content.embed.title, args),
footer: parse(content.embed.footer, args), description: parse(content.embed.description, args),
color: content.embed.color, footer: parse(content.embed.footer, args),
thumbnail: content.embed.thumbnail, color: content.embed.color,
timestamp: content.embed.timestamp, thumbnail: content.embed.thumbnail,
image: content.embed.image, timestamp: content.embed.timestamp,
} : null, image: content.embed.image,
}
: null,
url: args[3], url: args[3],
}; };
} }
@ -60,22 +67,34 @@ export async function sendUpload(user: User, image: Image, host: string) {
username: config.discord.username, username: config.discord.username,
avatar_url: config.discord.avatar_url, avatar_url: config.discord.avatar_url,
content: parsed.content ?? null, content: parsed.content ?? null,
embeds: parsed.embed ? [{ embeds: parsed.embed
title: parsed.embed.title ?? null, ? [
description: parsed.embed.description ?? null, {
url: parsed.url ?? null, title: parsed.embed.title ?? null,
timestamp: parsed.embed.timestamp ? image.created_at.toISOString() : null, description: parsed.embed.description ?? null,
color: parsed.embed.color ?? null, url: parsed.url ?? null,
footer: parsed.embed.footer ? { timestamp: parsed.embed.timestamp ? image.created_at.toISOString() : null,
text: parsed.embed.footer, color: parsed.embed.color ?? null,
} : null, footer: parsed.embed.footer
thumbnail: isImage && parsed.embed.thumbnail ? { ? {
url: parsed.url, text: parsed.embed.footer,
} : null, }
image: isImage && parsed.embed.image ? { : null,
url: parsed.url, thumbnail:
} : null, isImage && parsed.embed.thumbnail
}] : null, ? {
url: parsed.url,
}
: null,
image:
isImage && parsed.embed.image
? {
url: parsed.url,
}
: null,
},
]
: null,
}; };
const res = await fetch(config.discord.url, { const res = await fetch(config.discord.url, {
@ -88,7 +107,9 @@ export async function sendUpload(user: User, image: Image, host: string) {
if (!res.ok) { if (!res.ok) {
const text = await res.text(); const text = await res.text();
Logger.get('discord').error(`Failed to send upload notification to discord: ${res.status} ${res.statusText}`); Logger.get('discord').error(
`Failed to send upload notification to discord: ${res.status} ${res.statusText}`
);
Logger.get('discord').error(`Received response: ${text}`); Logger.get('discord').error(`Received response: ${text}`);
} }
@ -104,16 +125,22 @@ export async function sendShorten(user: User, url: Url, host: string) {
username: config.discord.username, username: config.discord.username,
avatar_url: config.discord.avatar_url, avatar_url: config.discord.avatar_url,
content: parsed.content ?? null, content: parsed.content ?? null,
embeds: parsed.embed ? [{ embeds: parsed.embed
title: parsed.embed.title ?? null, ? [
description: parsed.embed.description ?? null, {
url: parsed.url ?? null, title: parsed.embed.title ?? null,
timestamp: parsed.embed.timestamp ? url.created_at.toISOString() : null, description: parsed.embed.description ?? null,
color: parsed.embed.color ?? null, url: parsed.url ?? null,
footer: parsed.embed.footer ? { timestamp: parsed.embed.timestamp ? url.created_at.toISOString() : null,
text: parsed.embed.footer, color: parsed.embed.color ?? null,
} : null, footer: parsed.embed.footer
}] : null, ? {
text: parsed.embed.footer,
}
: null,
},
]
: null,
}; };
const res = await fetch(config.discord.url, { const res = await fetch(config.discord.url, {
@ -125,8 +152,10 @@ export async function sendShorten(user: User, url: Url, host: string) {
}); });
if (!res.ok) { if (!res.ok) {
Logger.get('discord').error(`Failed to send url shorten notification to discord: ${res.status} ${res.statusText}`); Logger.get('discord').error(
`Failed to send url shorten notification to discord: ${res.status} ${res.statusText}`
);
} }
return; return;
} }

View file

@ -1,45 +1,45 @@
// https://github.com/toptal/haste-server/blob/master/static/application.js#L167-L174 // https://github.com/toptal/haste-server/blob/master/static/application.js#L167-L174
// Popular extension map // Popular extension map
const exts = { const exts = {
'md': 'Markdown', md: 'Markdown',
'css': 'CSS', css: 'CSS',
'js': 'JavaScript', js: 'JavaScript',
'json': 'JSON', json: 'JSON',
'html': 'HTML', html: 'HTML',
'ts': 'TypeScript', ts: 'TypeScript',
'java': 'Java', java: 'Java',
'py': 'Python', py: 'Python',
'rb': 'Ruby', rb: 'Ruby',
'sh': 'Shell', sh: 'Shell',
'php': 'PHP', php: 'PHP',
'pl': 'Perl', pl: 'Perl',
'sql': 'SQL', sql: 'SQL',
'xml': 'XML', xml: 'XML',
'yml': 'YAML', yml: 'YAML',
'yaml': 'YAML', yaml: 'YAML',
'c': 'C', c: 'C',
'cpp': 'C++', cpp: 'C++',
'cs': 'C#', cs: 'C#',
'go': 'Go', go: 'Go',
'h': 'C/C++ Header', h: 'C/C++ Header',
'txt': 'Text', txt: 'Text',
'dockerfile': 'Dockerfile', dockerfile: 'Dockerfile',
'toml': 'TOML', toml: 'TOML',
'ini': 'INI', ini: 'INI',
'bat': 'Batch File', bat: 'Batch File',
'tex': 'TeX', tex: 'TeX',
'r': 'R', r: 'R',
'lua': 'Lua', lua: 'Lua',
'ps1': 'PowerShell', ps1: 'PowerShell',
'rst': 'reStructuredText', rst: 'reStructuredText',
'rs': 'Rust', rs: 'Rust',
'swift': 'Swift', swift: 'Swift',
'scss': 'SCSS', scss: 'SCSS',
'less': 'LESS', less: 'LESS',
'scala': 'Scala', scala: 'Scala',
'kotlin': 'Kotlin', kotlin: 'Kotlin',
'vb': 'Visual Basic', vb: 'Visual Basic',
'vim': 'Vim Script', vim: 'Vim Script',
}; };
export default exts; export default exts;

View file

@ -1,4 +1,8 @@
export default async function useFetch(url: string, method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'GET', body: Record<string, any> = null) { export default async function useFetch(
url: string,
method: 'GET' | 'POST' | 'PATCH' | 'DELETE' = 'GET',
body: Record<string, any> = null
) {
const headers = {}; const headers = {};
if (body) headers['content-type'] = 'application/json'; if (body) headers['content-type'] = 'application/json';
@ -9,4 +13,4 @@ export default async function useFetch(url: string, method: 'GET' | 'POST' | 'PA
}); });
return res.json(); return res.json();
} }

View file

@ -11,11 +11,11 @@ export default function login() {
async function load() { async function load() {
setLoading(true); setLoading(true);
const res = await useFetch('/api/user'); const res = await useFetch('/api/user');
if (res.error) { if (res.error) {
if (res.error === 'oauth token expired') return router.push(res.redirect_uri); if (res.error === 'oauth token expired') return router.push(res.redirect_uri);
return router.push('/auth/login?url=' + router.route); return router.push('/auth/login?url=' + router.route);
} }
@ -29,4 +29,4 @@ export default function login() {
}, []); }, []);
return { loading }; return { loading };
} }

View file

@ -10,8 +10,7 @@ export default class Logger {
public name: string; public name: string;
static get(clas: any) { static get(clas: any) {
if (typeof clas !== 'function') if (typeof clas !== 'function') if (typeof clas !== 'string') throw new Error('not string/function');
if (typeof clas !== 'string') throw new Error('not string/function');
const name = clas.name ?? clas; const name = clas.name ?? clas;
@ -28,11 +27,7 @@ export default class Logger {
error(...args: any[]) { error(...args: any[]) {
console.log( console.log(
this.formatMessage( this.formatMessage(LoggerLevel.ERROR, this.name, args.map((error) => error.stack ?? error).join(' '))
LoggerLevel.ERROR,
this.name,
args.map((error) => error.stack ?? error).join(' ')
)
); );
} }
@ -49,4 +44,4 @@ export default class Logger {
return red('error'); return red('error');
} }
} }
} }

View file

@ -3,21 +3,23 @@ import { discord_auth, github_auth } from 'lib/oauth';
import { notNull } from 'lib/util'; import { notNull } from 'lib/util';
import { GetServerSideProps } from 'next'; import { GetServerSideProps } from 'next';
export const getServerSideProps: GetServerSideProps = async ctx => { export const getServerSideProps: GetServerSideProps = async (ctx) => {
// this entire thing will also probably change before the stable release // this entire thing will also probably change before the stable release
const ghEnabled = notNull(config.oauth?.github_client_id, config.oauth?.github_client_secret); const ghEnabled = notNull(config.oauth?.github_client_id, config.oauth?.github_client_secret);
const discEnabled = notNull(config.oauth?.discord_client_id, config.oauth?.discord_client_secret); const discEnabled = notNull(config.oauth?.discord_client_id, config.oauth?.discord_client_secret);
const oauth_providers = []; const oauth_providers = [];
if (ghEnabled) oauth_providers.push({ if (ghEnabled)
name: 'GitHub', oauth_providers.push({
url: '/api/auth/oauth/github', name: 'GitHub',
}); url: '/api/auth/oauth/github',
if (discEnabled) oauth_providers.push({ });
name: 'Discord', if (discEnabled)
url: '/api/auth/oauth/discord', oauth_providers.push({
}); name: 'Discord',
url: '/api/auth/oauth/discord',
});
return { return {
props: { props: {
@ -29,4 +31,4 @@ export const getServerSideProps: GetServerSideProps = async ctx => {
oauth_providers: JSON.stringify(oauth_providers), oauth_providers: JSON.stringify(oauth_providers),
}, },
}; };
}; };

View file

@ -21,7 +21,7 @@ export type NextApiReq = NextApiRequest & {
getCookie: (name: string) => string | null; getCookie: (name: string) => string | null;
cleanCookie: (name: string) => void; cleanCookie: (name: string) => void;
files?: NextApiFile[]; files?: NextApiFile[];
} };
export type NextApiRes = NextApiResponse & { export type NextApiRes = NextApiResponse & {
error: (message: string) => void; error: (message: string) => void;
@ -30,95 +30,108 @@ export type NextApiRes = NextApiResponse & {
json: (json: Record<string, any>, status?: number) => void; json: (json: Record<string, any>, status?: number) => void;
ratelimited: (remaining: number) => void; ratelimited: (remaining: number) => void;
setCookie: (name: string, value: unknown, options: CookieSerializeOptions) => void; setCookie: (name: string, value: unknown, options: CookieSerializeOptions) => void;
}
export const withZipline = (handler: (req: NextApiRequest, res: NextApiResponse) => unknown) => (req: NextApiReq, res: NextApiRes) => {
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Content-Allow-Methods', 'GET,HEAD,POST,OPTIONS');
res.setHeader('Access-Control-Max-Age', '86400');
res.error = (message: string) => {
res.json({
error: message,
}, 500);
};
res.forbid = (message: string, extra: any = {}) => {
res.json({
error: '403: ' + message,
...extra,
}, 403);
};
res.bad = (message: string) => {
res.json({
error: '401: ' + message,
}, 401);
};
res.ratelimited = (remaining: number) => {
res.status(429);
res.setHeader('X-Ratelimit-Remaining', Math.floor(remaining / 1000));
res.json({
error: '429: ratelimited',
});
};
res.json = (json: any, status: number = 200) => {
res.setHeader('Content-Type', 'application/json');
res.status(status);
res.end(JSON.stringify(json));
};
req.getCookie = (name: string) => {
const cookie = req.cookies[name];
if (!cookie) return null;
const unsigned = unsign64(cookie, config.core.secret);
return unsigned ? unsigned : null;
};
req.cleanCookie = (name: string) => {
res.setHeader('Set-Cookie', serialize(name, '', {
path: '/',
expires: new Date(1),
maxAge: undefined,
}));
};
req.user = async () => {
try {
const userId = req.getCookie('user');
if (!userId) return null;
const user = await prisma.user.findFirst({
where: {
id: Number(userId),
},
});
if (!user) return null;
return user;
} catch (e) {
if (e.code && e.code === 'ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH') {
req.cleanCookie('user');
return null;
}
}
};
res.setCookie = (name: string, value: unknown, options?: CookieSerializeOptions) => setCookie(res, name, value, options || {});
return handler(req, res);
}; };
export const withZipline =
(handler: (req: NextApiRequest, res: NextApiResponse) => unknown) => (req: NextApiReq, res: NextApiRes) => {
res.setHeader('Content-Type', 'application/json');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Content-Allow-Methods', 'GET,HEAD,POST,OPTIONS');
res.setHeader('Access-Control-Max-Age', '86400');
res.error = (message: string) => {
res.json(
{
error: message,
},
500
);
};
res.forbid = (message: string, extra: any = {}) => {
res.json(
{
error: '403: ' + message,
...extra,
},
403
);
};
res.bad = (message: string) => {
res.json(
{
error: '401: ' + message,
},
401
);
};
res.ratelimited = (remaining: number) => {
res.status(429);
res.setHeader('X-Ratelimit-Remaining', Math.floor(remaining / 1000));
res.json({
error: '429: ratelimited',
});
};
res.json = (json: any, status: number = 200) => {
res.setHeader('Content-Type', 'application/json');
res.status(status);
res.end(JSON.stringify(json));
};
req.getCookie = (name: string) => {
const cookie = req.cookies[name];
if (!cookie) return null;
const unsigned = unsign64(cookie, config.core.secret);
return unsigned ? unsigned : null;
};
req.cleanCookie = (name: string) => {
res.setHeader(
'Set-Cookie',
serialize(name, '', {
path: '/',
expires: new Date(1),
maxAge: undefined,
})
);
};
req.user = async () => {
try {
const userId = req.getCookie('user');
if (!userId) return null;
const user = await prisma.user.findFirst({
where: {
id: Number(userId),
},
});
if (!user) return null;
return user;
} catch (e) {
if (e.code && e.code === 'ERR_CRYPTO_TIMING_SAFE_EQUAL_LENGTH') {
req.cleanCookie('user');
return null;
}
}
};
res.setCookie = (name: string, value: unknown, options?: CookieSerializeOptions) =>
setCookie(res, name, value, options || {});
return handler(req, res);
};
export const setCookie = ( export const setCookie = (
res: NextApiResponse, res: NextApiResponse,
name: string, name: string,
value: unknown, value: unknown,
options: CookieSerializeOptions = {} options: CookieSerializeOptions = {}
) => { ) => {
if ('maxAge' in options) { if ('maxAge' in options) {
options.expires = new Date(Date.now() + options.maxAge * 1000); options.expires = new Date(Date.now() + options.maxAge * 1000);
options.maxAge /= 1000; options.maxAge /= 1000;

View file

@ -1,12 +1,12 @@
import { readFile } from 'fs/promises'; import { readFile } from 'fs/promises';
export type Mimes = [string, string[]][] export type Mimes = [string, string[]][];
export async function guess(extension: string): Promise<string> { export async function guess(extension: string): Promise<string> {
const mimes: Mimes = JSON.parse(await readFile('./mimes.json', 'utf8')); const mimes: Mimes = JSON.parse(await readFile('./mimes.json', 'utf8'));
const mime = mimes.find(x => x[0] === extension); const mime = mimes.find((x) => x[0] === extension);
if (!mime) return 'application/octet-stream'; if (!mime) return 'application/octet-stream';
return mime[1][0]; return mime[1][0];
} }

View file

@ -1,9 +1,10 @@
export const github_auth = { export const github_auth = {
oauth_url: (clientId: string) => `https://github.com/login/oauth/authorize?client_id=${clientId}&scope=user`, oauth_url: (clientId: string) =>
`https://github.com/login/oauth/authorize?client_id=${clientId}&scope=user`,
oauth_user: async (access_token: string) => { oauth_user: async (access_token: string) => {
const res = await fetch('https://api.github.com/user', { const res = await fetch('https://api.github.com/user', {
headers: { headers: {
'Authorization': `Bearer ${access_token}`, Authorization: `Bearer ${access_token}`,
}, },
}); });
if (!res.ok) return null; if (!res.ok) return null;
@ -13,15 +14,18 @@ export const github_auth = {
}; };
export const discord_auth = { export const discord_auth = {
oauth_url: (clientId: string, origin: string) => `https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(`${origin}/api/auth/oauth/discord`)}&response_type=code&scope=identify`, oauth_url: (clientId: string, origin: string) =>
`https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(
`${origin}/api/auth/oauth/discord`
)}&response_type=code&scope=identify`,
oauth_user: async (access_token: string) => { oauth_user: async (access_token: string) => {
const res = await fetch('https://discord.com/api/users/@me', { const res = await fetch('https://discord.com/api/users/@me', {
headers: { headers: {
'Authorization': `Bearer ${access_token}`, Authorization: `Bearer ${access_token}`,
}, },
}); });
if (!res.ok) return null; if (!res.ok) return null;
return res.json(); return res.json();
}, },
}; };

Some files were not shown because too many files have changed in this diff Show more