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:
parent
cb7dacd089
commit
af0cd26ea0
165 changed files with 3635 additions and 10100 deletions
|
@ -1,36 +1,18 @@
|
|||
{
|
||||
"extends": [
|
||||
"next",
|
||||
"next/core-web-vitals"
|
||||
],
|
||||
"extends": ["next", "next/core-web-vitals", "plugin:prettier/recommended"],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
"SwitchCase": 1
|
||||
}
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": [
|
||||
"error",
|
||||
"single"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"always-multiline"
|
||||
],
|
||||
"jsx-quotes": [
|
||||
"error",
|
||||
"prefer-single"
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true
|
||||
}
|
||||
],
|
||||
"semi": ["error", "always"],
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"jsx-quotes": ["error", "prefer-single"],
|
||||
"indent": "off",
|
||||
"react/prop-types": "off",
|
||||
"react-hooks/rules-of-hooks": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
|
@ -48,4 +30,4 @@
|
|||
"jsx-a11y/alt-text": "off",
|
||||
"react/display-name": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
5
.prettierrc.json
Normal file
5
.prettierrc.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 110
|
||||
}
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -2,4 +2,4 @@
|
|||
"editor.tabSize": 2,
|
||||
"files.eol": "\n",
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ nodeLinker: node-modules
|
|||
|
||||
plugins:
|
||||
- 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
|
||||
|
||||
checksumBehavior: "update"
|
||||
checksumBehavior: 'update'
|
||||
|
|
|
@ -1,18 +1,23 @@
|
|||
# Contributing
|
||||
|
||||
## 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):
|
||||
* The steps to reproduce the bug
|
||||
* Logs of Zipline
|
||||
* The version of Zipline
|
||||
* Your OS & Browser including server OS
|
||||
* What you were expecting to see
|
||||
|
||||
- The steps to reproduce the bug
|
||||
- Logs of Zipline
|
||||
- The version of Zipline
|
||||
- Your OS & Browser including server OS
|
||||
- What you were expecting to see
|
||||
|
||||
## Feature requests
|
||||
|
||||
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)
|
||||
|
||||
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.
|
||||
|
|
46
README.md
46
README.md
|
@ -1,20 +1,21 @@
|
|||
<div align="center">
|
||||
<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)
|
||||
![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)
|
||||
[![Discord](https://img.shields.io/discord/729771078196527176?color=%23777ed3&label=discord&logo=discord&logoColor=white&style=flat)](https://discord.gg/EAhCRfGxCF)
|
||||
![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)
|
||||
![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)
|
||||
|
||||
![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 (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)
|
||||
![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 (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>
|
||||
|
||||
## Features
|
||||
|
||||
- Configurable
|
||||
- Fast
|
||||
- Built with Next.js & React
|
||||
|
@ -35,6 +36,7 @@
|
|||
# Usage
|
||||
|
||||
## Install & run with Docker
|
||||
|
||||
This section requires [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/).
|
||||
|
||||
```shell
|
||||
|
@ -45,11 +47,14 @@ docker-compose up -d
|
|||
```
|
||||
|
||||
### 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.
|
||||
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
|
||||
|
||||
This section requires [nodejs](https://nodejs.org), [yarn](https://yarnpkg.com/) or [npm](https://npmjs.com).
|
||||
|
||||
```shell
|
||||
git clone https://github.com/diced/zipline
|
||||
cd zipline
|
||||
|
@ -63,6 +68,7 @@ yarn start
|
|||
```
|
||||
|
||||
# NGINX Proxy
|
||||
|
||||
This section requires [NGINX](https://nginx.org/).
|
||||
|
||||
```nginx
|
||||
|
@ -81,14 +87,17 @@ server {
|
|||
```
|
||||
|
||||
# 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.
|
||||
|
||||
# ShareX (Windows)
|
||||
|
||||
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)
|
||||
|
||||
# Flameshot (Linux)
|
||||
|
||||
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).
|
||||
|
@ -104,17 +113,22 @@ curl -H "Content-Type: multipart/form-data" -H "authorization: $TOKEN" -F file=@
|
|||
# Contributing
|
||||
|
||||
## 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):
|
||||
* The steps to reproduce the bug
|
||||
* Logs of Zipline
|
||||
* The version of Zipline
|
||||
* Your OS & Browser including server OS
|
||||
* What you were expecting to see
|
||||
|
||||
- The steps to reproduce the bug
|
||||
- Logs of Zipline
|
||||
- The version of Zipline
|
||||
- Your OS & Browser including server OS
|
||||
- What you were expecting to see
|
||||
|
||||
## Feature requests
|
||||
|
||||
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)
|
||||
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.
|
||||
|
|
|
@ -9,4 +9,5 @@
|
|||
| < 2 | :x: |
|
||||
|
||||
## 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.
|
||||
|
|
|
@ -36,4 +36,4 @@ services:
|
|||
- 'postgres'
|
||||
|
||||
volumes:
|
||||
pg_data:
|
||||
pg_data:
|
||||
|
|
|
@ -34,4 +34,4 @@ services:
|
|||
- 'postgres'
|
||||
|
||||
volumes:
|
||||
pg_data:
|
||||
pg_data:
|
||||
|
|
|
@ -40,4 +40,4 @@ const { rm } = require('fs/promises');
|
|||
sourcemap: true,
|
||||
minify: false,
|
||||
});
|
||||
})();
|
||||
})();
|
||||
|
|
9680
mimes.json
9680
mimes.json
File diff suppressed because it is too large
Load diff
|
@ -23,4 +23,4 @@ module.exports = {
|
|||
},
|
||||
poweredByHeader: false,
|
||||
reactStrictMode: true,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"build-ci": "cross-env ZIPLINE_DOCKER_BUILD=1 npm-run-all build:schema build:next",
|
||||
"build:next": "next build",
|
||||
"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",
|
||||
"start": "tsx src/server",
|
||||
"lint": "next lint",
|
||||
|
@ -69,8 +70,10 @@
|
|||
"esbuild": "^0.14.44",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-config-next": "12.1.6",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-node": "^10.8.1",
|
||||
"tsx": "^3.8.0",
|
||||
"typescript": "^4.7.3"
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Card as MCard, Title } from '@mantine/core';
|
||||
|
||||
|
||||
export default function Card({ name, children, ...other }) {
|
||||
return (
|
||||
<MCard p='md' shadow='sm' {...other}>
|
||||
|
@ -8,4 +7,4 @@ export default function Card({ name, children, ...other }) {
|
|||
{children}
|
||||
</MCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,5 @@ const useStyles = createStyles((theme, { size }: { size: MantineSize }) => ({
|
|||
export default function CodeInput({ ...props }) {
|
||||
const { classes } = useStyles({ size: 'md' }, { name: 'CodeInput' });
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
classNames={{ input: classes.input }}
|
||||
autoComplete='nope'
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
return <Textarea classNames={{ input: classes.input }} autoComplete='nope' {...props} />;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,19 @@ import { showNotification } from '@mantine/notifications';
|
|||
import { relativeTime } from 'lib/utils/client';
|
||||
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
|
||||
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 Type from './Type';
|
||||
import Link from './Link';
|
||||
|
@ -76,35 +88,33 @@ export default function File({ image, updateImages, disableMediaPreview }) {
|
|||
};
|
||||
|
||||
const handleFavorite = async () => {
|
||||
favoriteFile.mutate({ id: image.id, favorite: !image.favorite }, {
|
||||
onSuccess: () => {
|
||||
showNotification({
|
||||
title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'),
|
||||
message: '',
|
||||
icon: <StarIcon />,
|
||||
});
|
||||
},
|
||||
favoriteFile.mutate(
|
||||
{ id: image.id, favorite: !image.favorite },
|
||||
{
|
||||
onSuccess: () => {
|
||||
showNotification({
|
||||
title: 'Image is now ' + (!image.favorite ? 'favorited' : 'unfavorited'),
|
||||
message: '',
|
||||
icon: <StarIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
onError: (res: any) => {
|
||||
showNotification({
|
||||
title: 'Failed to favorite file',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
},
|
||||
});
|
||||
onError: (res: any) => {
|
||||
showNotification({
|
||||
title: 'Failed to favorite file',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
console.log(image);
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
opened={open}
|
||||
onClose={() => setOpen(false)}
|
||||
title={<Title>{image.file}</Title>}
|
||||
size='xl'
|
||||
>
|
||||
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>{image.file}</Title>} size='xl'>
|
||||
<LoadingOverlay visible={loading} />
|
||||
<Stack>
|
||||
<Type
|
||||
|
@ -120,13 +130,19 @@ export default function File({ image, updateImages, disableMediaPreview }) {
|
|||
<FileMeta Icon={FileIcon} title='Name' subtitle={image.file} />
|
||||
<FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} />
|
||||
<FileMeta Icon={EyeIcon} title='Views' subtitle={image.views} />
|
||||
<FileMeta Icon={CalendarIcon} title='Uploaded at' subtitle={new Date(image.created_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={CalendarIcon}
|
||||
title='Uploaded at'
|
||||
subtitle={new Date(image.created_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} />
|
||||
</Stack>
|
||||
</Stack>
|
||||
|
@ -145,8 +161,20 @@ export default function File({ image, updateImages, disableMediaPreview }) {
|
|||
<LoadingOverlay visible={loading} />
|
||||
<Type
|
||||
file={image}
|
||||
sx={{ minHeight: 200, maxHeight: 320, fontSize: 70, width: '100%', cursor: 'pointer' }}
|
||||
style={{ minHeight: 200, maxHeight: 320, fontSize: 70, width: '100%', cursor: 'pointer' }}
|
||||
sx={{
|
||||
minHeight: 200,
|
||||
maxHeight: 320,
|
||||
fontSize: 70,
|
||||
width: '100%',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
style={{
|
||||
minHeight: 200,
|
||||
maxHeight: 320,
|
||||
fontSize: 70,
|
||||
width: '100%',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
src={image.url}
|
||||
alt={image.file}
|
||||
onClick={() => setOpen(true)}
|
||||
|
@ -156,4 +184,4 @@ export default function File({ image, updateImages, disableMediaPreview }) {
|
|||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 { useModals } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
|
@ -9,7 +32,24 @@ import { useRecoilState } from 'recoil';
|
|||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
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';
|
||||
|
||||
function MenuItemLink(props) {
|
||||
|
@ -23,7 +63,7 @@ function MenuItemLink(props) {
|
|||
function MenuItem(props) {
|
||||
return (
|
||||
<UnstyledButton
|
||||
sx={theme => ({
|
||||
sx={(theme) => ({
|
||||
display: 'block',
|
||||
width: '100%',
|
||||
padding: 5,
|
||||
|
@ -31,30 +71,32 @@ function MenuItem(props) {
|
|||
color: props.color
|
||||
? theme.fn.themeColor(props.color, theme.colorScheme === 'dark' ? 5 : 7)
|
||||
: theme.colorScheme === 'dark'
|
||||
? theme.colors.dark[0]
|
||||
: theme.black,
|
||||
? theme.colors.dark[0]
|
||||
: theme.black,
|
||||
'&:hover': {
|
||||
backgroundColor: props.color
|
||||
? theme.fn.rgba(
|
||||
theme.fn.themeColor(props.color, theme.colorScheme === 'dark' ? 9 : 0),
|
||||
theme.colorScheme === 'dark' ? 0.2 : 1
|
||||
)
|
||||
theme.fn.themeColor(props.color, theme.colorScheme === 'dark' ? 9 : 0),
|
||||
theme.colorScheme === 'dark' ? 0.2 : 1
|
||||
)
|
||||
: theme.colorScheme === 'dark'
|
||||
? theme.fn.rgba(theme.colors.dark[3], 0.35)
|
||||
: theme.colors.gray[0],
|
||||
? theme.fn.rgba(theme.colors.dark[3], 0.35)
|
||||
: theme.colors.gray[0],
|
||||
},
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
<Group noWrap>
|
||||
<Box sx={theme => ({
|
||||
marginRight: theme.spacing.xs / 4,
|
||||
paddingLeft: theme.spacing.xs / 2,
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
marginRight: theme.spacing.xs / 4,
|
||||
paddingLeft: theme.spacing.xs / 2,
|
||||
|
||||
'& *': {
|
||||
display: 'block',
|
||||
},
|
||||
})}>
|
||||
'& *': {
|
||||
display: 'block',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{props.icon}
|
||||
</Box>
|
||||
<Text size='sm'>{props.children}</Text>
|
||||
|
@ -101,13 +143,13 @@ const admin_items = [
|
|||
icon: <UserIcon size={18} />,
|
||||
text: 'Users',
|
||||
link: '/dashboard/users',
|
||||
if: props => true,
|
||||
if: (props) => true,
|
||||
},
|
||||
{
|
||||
icon: <TagIcon size={18} />,
|
||||
text: '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 clipboard = useClipboard();
|
||||
|
||||
const handleUpdateTheme = async value => {
|
||||
const handleUpdateTheme = async (value) => {
|
||||
const newUser = await useFetch('/api/user', 'PATCH', {
|
||||
systemTheme: value || 'dark_blue',
|
||||
});
|
||||
|
@ -146,74 +188,70 @@ export default function Layout({ children, props }) {
|
|||
});
|
||||
};
|
||||
|
||||
const openResetToken = () => modals.openConfirmModal({
|
||||
title: 'Reset Token',
|
||||
children: (
|
||||
<Text size='sm'>
|
||||
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 () => {
|
||||
const a = await useFetch('/api/user/token', 'PATCH');
|
||||
if (!a.success) {
|
||||
setToken(a.success);
|
||||
const openResetToken = () =>
|
||||
modals.openConfirmModal({
|
||||
title: 'Reset Token',
|
||||
children: (
|
||||
<Text size='sm'>
|
||||
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 () => {
|
||||
const a = await useFetch('/api/user/token', 'PATCH');
|
||||
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'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 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.',
|
||||
title: 'Token Copied',
|
||||
message: 'Your token has been copied to your clipboard.',
|
||||
color: 'green',
|
||||
icon: <CheckIcon />,
|
||||
});
|
||||
}
|
||||
|
||||
modals.closeAll();
|
||||
},
|
||||
});
|
||||
|
||||
const openCopyToken = () => modals.openConfirmModal({
|
||||
title: 'Copy Token',
|
||||
children: (
|
||||
<Text size='sm'>
|
||||
Make sure you don'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();
|
||||
},
|
||||
});
|
||||
modals.closeAll();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
navbarOffsetBreakpoint='sm'
|
||||
fixed
|
||||
navbar={
|
||||
<Navbar
|
||||
pt='sm'
|
||||
hiddenBreakpoint='sm'
|
||||
hidden={!opened}
|
||||
width={{ sm: 200, lg: 230 }}
|
||||
>
|
||||
<Navbar.Section
|
||||
grow
|
||||
component={ScrollArea}
|
||||
>
|
||||
<Navbar pt='sm' hiddenBreakpoint='sm' hidden={!opened} width={{ sm: 200, lg: 230 }}>
|
||||
<Navbar.Section grow component={ScrollArea}>
|
||||
{items.map(({ icon, text, link }) => (
|
||||
<Link href={link} key={text} passHref>
|
||||
<NavLink
|
||||
|
@ -230,34 +268,38 @@ export default function Layout({ children, props }) {
|
|||
label='Administration'
|
||||
icon={<SettingsIcon />}
|
||||
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 }) => (
|
||||
<Link href={link} key={text} passHref>
|
||||
<NavLink
|
||||
component='a'
|
||||
label={text}
|
||||
icon={icon}
|
||||
active={router.pathname === link}
|
||||
variant='light'
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
{admin_items
|
||||
.filter((x) => x.if(props))
|
||||
.map(({ icon, text, link }) => (
|
||||
<Link href={link} key={text} passHref>
|
||||
<NavLink
|
||||
component='a'
|
||||
label={text}
|
||||
icon={icon}
|
||||
active={router.pathname === link}
|
||||
variant='light'
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</NavLink>
|
||||
)}
|
||||
</Navbar.Section>
|
||||
<Navbar.Section>
|
||||
{external_links.length ? external_links.map(({ label, link }, i) => (
|
||||
<Link href={link} passHref key={i}>
|
||||
<NavLink
|
||||
label={label}
|
||||
component='a'
|
||||
target='_blank'
|
||||
variant='light'
|
||||
icon={<ExternalLinkIcon />}
|
||||
/>
|
||||
</Link>
|
||||
)) : null}
|
||||
{external_links.length
|
||||
? external_links.map(({ label, link }, i) => (
|
||||
<Link href={link} passHref key={i}>
|
||||
<NavLink
|
||||
label={label}
|
||||
component='a'
|
||||
target='_blank'
|
||||
variant='light'
|
||||
icon={<ExternalLinkIcon />}
|
||||
/>
|
||||
</Link>
|
||||
))
|
||||
: null}
|
||||
</Navbar.Section>
|
||||
{version.isSuccess ? (
|
||||
<Navbar.Section>
|
||||
|
@ -295,16 +337,12 @@ export default function Layout({ children, props }) {
|
|||
</MediaQuery>
|
||||
<Title ml='sm'>{title}</Title>
|
||||
<Box sx={{ marginLeft: 'auto', marginRight: 0 }}>
|
||||
<Popover
|
||||
position='bottom-end'
|
||||
opened={open}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<Popover position='bottom-end' opened={open} onClose={() => setOpen(false)}>
|
||||
<Popover.Target>
|
||||
<Button
|
||||
leftIcon={avatar ? <Image src={avatar} height={32} radius='md' /> : <SettingsIcon />}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
sx={t => ({
|
||||
sx={(t) => ({
|
||||
backgroundColor: 'inherit',
|
||||
'&:hover': {
|
||||
backgroundColor: t.other.hover,
|
||||
|
@ -320,33 +358,59 @@ export default function Layout({ children, props }) {
|
|||
|
||||
<Popover.Dropdown p={4} mr='md' sx={{ minWidth: '200px' }}>
|
||||
<Stack spacing={2}>
|
||||
<Text sx={{
|
||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
|
||||
fontWeight: 500,
|
||||
fontSize: theme.fontSizes.sm,
|
||||
padding: `${theme.spacing.xs / 2}px ${theme.spacing.sm}px`,
|
||||
cursor: 'default',
|
||||
}}
|
||||
<Text
|
||||
sx={{
|
||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[2] : theme.colors.gray[6],
|
||||
fontWeight: 500,
|
||||
fontSize: theme.fontSizes.sm,
|
||||
padding: `${theme.spacing.xs / 2}px ${theme.spacing.sm}px`,
|
||||
cursor: 'default',
|
||||
}}
|
||||
>
|
||||
{user.username}
|
||||
</Text>
|
||||
<MenuItemLink icon={<SettingsIcon />} href='/dashboard/manage'>Manage Account</MenuItemLink>
|
||||
<MenuItem icon={<CopyIcon />} onClick={() => { setOpen(false); openCopyToken(); }}>Copy Token</MenuItem>
|
||||
<MenuItem icon={<DeleteIcon />} onClick={() => { setOpen(false); openResetToken(); }} color='red'>Reset Token</MenuItem>
|
||||
<MenuItemLink icon={<LogoutIcon />} href='/auth/logout' color='red'>Logout</MenuItemLink>
|
||||
<MenuItemLink icon={<SettingsIcon />} href='/dashboard/manage'>
|
||||
Manage Account
|
||||
</MenuItemLink>
|
||||
<MenuItem
|
||||
icon={<CopyIcon />}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
openCopyToken();
|
||||
}}
|
||||
>
|
||||
Copy Token
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
icon={<DeleteIcon />}
|
||||
onClick={() => {
|
||||
setOpen(false);
|
||||
openResetToken();
|
||||
}}
|
||||
color='red'
|
||||
>
|
||||
Reset Token
|
||||
</MenuItem>
|
||||
<MenuItemLink icon={<LogoutIcon />} href='/auth/logout' color='red'>
|
||||
Logout
|
||||
</MenuItemLink>
|
||||
<Divider
|
||||
variant='solid'
|
||||
my={theme.spacing.xs / 2}
|
||||
sx={theme => ({
|
||||
sx={(theme) => ({
|
||||
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`,
|
||||
})}
|
||||
/>
|
||||
<MenuItem icon={<PencilIcon />}>
|
||||
<Select
|
||||
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}
|
||||
onChange={handleUpdateTheme}
|
||||
/>
|
||||
|
@ -363,7 +427,7 @@ export default function Layout({ children, props }) {
|
|||
withBorder
|
||||
p='md'
|
||||
shadow='xs'
|
||||
sx={t => ({
|
||||
sx={(t) => ({
|
||||
borderColor: t.colorScheme === 'dark' ? t.colors.dark[5] : t.colors.dark[0],
|
||||
})}
|
||||
>
|
||||
|
@ -371,4 +435,4 @@ export default function Layout({ children, props }) {
|
|||
</Paper>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import { NextLink as Link } from '@mantine/next';
|
||||
|
||||
export default Link;
|
||||
export default Link;
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { Text } from '@mantine/core';
|
||||
|
||||
export default function MutedText({ children, ...props }) {
|
||||
return <Text color='dimmed' size='xl' {...props}>{children}</Text>;
|
||||
}
|
||||
return (
|
||||
<Text color='dimmed' size='xl' {...props}>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,12 +6,7 @@ import { CheckIcon, CrossIcon } from './icons';
|
|||
|
||||
function PasswordRequirement({ meets, label }: { meets: boolean; label: string }) {
|
||||
return (
|
||||
<Text
|
||||
color={meets ? 'teal' : 'red'}
|
||||
sx={{ display: 'flex', alignItems: 'center' }}
|
||||
mt='sm'
|
||||
size='sm'
|
||||
>
|
||||
<Text color={meets ? 'teal' : 'red'} sx={{ display: 'flex', alignItems: 'center' }} mt='sm' size='sm'>
|
||||
{meets ? <CheckIcon /> : <CrossIcon />} <Box ml='md'>{label}</Box>
|
||||
</Text>
|
||||
);
|
||||
|
@ -60,10 +55,7 @@ export default function PasswordStrength({ value, setValue, setStrength, ...prop
|
|||
}}
|
||||
>
|
||||
<Popover.Target>
|
||||
<div
|
||||
onFocusCapture={() => setPopoverOpened(true)}
|
||||
onBlurCapture={() => setPopoverOpened(false)}
|
||||
>
|
||||
<div onFocusCapture={() => setPopoverOpened(true)} onBlurCapture={() => setPopoverOpened(false)}>
|
||||
<PasswordInput
|
||||
label='Password'
|
||||
description='A strong password should include letters in lower and uppercase, at least 1 number, at least 1 special symbol'
|
||||
|
|
|
@ -7,18 +7,16 @@ export function SmallTable({ rows, columns }) {
|
|||
<Table highlightOnHover>
|
||||
<thead>
|
||||
<tr>
|
||||
{columns.map(col => (
|
||||
{columns.map((col) => (
|
||||
<th key={randomId()}>{col.name}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map(row => (
|
||||
{rows.map((row) => (
|
||||
<tr key={randomId()}>
|
||||
{columns.map(col => (
|
||||
<td key={randomId()}>
|
||||
{col.format ? col.format(row[col.id]) : row[col.id]}
|
||||
</td>
|
||||
{columns.map((col) => (
|
||||
<td key={randomId()}>{col.format ? col.format(row[col.id]) : row[col.id]}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
|
@ -26,4 +24,4 @@ export function SmallTable({ rows, columns }) {
|
|||
</Table>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,9 +29,9 @@ const useStyles = createStyles((theme) => ({
|
|||
}));
|
||||
|
||||
interface StatsGridProps {
|
||||
stat: {
|
||||
title: string;
|
||||
icon: React.ReactNode,
|
||||
stat: {
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
value: string;
|
||||
desc: string;
|
||||
diff?: number;
|
||||
|
@ -53,27 +53,14 @@ export default function StatCard({ stat }: StatsGridProps) {
|
|||
|
||||
<Group align='flex-end' spacing='xs' mt={25}>
|
||||
<Text className={classes.value}>{stat.value}</Text>
|
||||
{
|
||||
typeof stat.diff == 'number' && (
|
||||
<>
|
||||
<Text
|
||||
color={stat.diff >= 0 ? 'teal' : 'red'}
|
||||
size='sm'
|
||||
weight={500}
|
||||
className={classes.diff}
|
||||
>
|
||||
<span>{stat.diff === Infinity ? '∞' : stat.diff}%</span>
|
||||
{
|
||||
stat.diff >= 0 ? (
|
||||
<ArrowUpRight size={16} />
|
||||
) : (
|
||||
<ArrowDownRight size={16} />
|
||||
)
|
||||
}
|
||||
</Text>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{typeof stat.diff == 'number' && (
|
||||
<>
|
||||
<Text color={stat.diff >= 0 ? 'teal' : 'red'} size='sm' weight={500} className={classes.diff}>
|
||||
<span>{stat.diff === Infinity ? '∞' : stat.diff}%</span>
|
||||
{stat.diff >= 0 ? <ArrowUpRight size={16} /> : <ArrowDownRight size={16} />}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
<Text size='xs' color='dimmed' mt={7}>
|
||||
|
@ -81,4 +68,4 @@ export default function StatCard({ stat }: StatsGridProps) {
|
|||
</Text>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import { useRecoilValue } from 'recoil';
|
|||
import { userSelector } from 'lib/recoil/user';
|
||||
|
||||
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,
|
||||
light_blue,
|
||||
dark,
|
||||
|
@ -34,17 +34,17 @@ export const themes = {
|
|||
};
|
||||
|
||||
export const friendlyThemeName = {
|
||||
'system': 'System Theme',
|
||||
'dark_blue': 'Dark Blue',
|
||||
'light_blue': 'Light Blue',
|
||||
'dark': 'Very Dark',
|
||||
'ayu_dark': 'Ayu Dark',
|
||||
'ayu_mirage': 'Ayu Mirage',
|
||||
'ayu_light': 'Ayu Light',
|
||||
'nord': 'Nord',
|
||||
'dracula': 'Dracula',
|
||||
'matcha_dark_azul': 'Matcha Dark Azul',
|
||||
'qogir_dark': 'Qogir Dark',
|
||||
system: 'System Theme',
|
||||
dark_blue: 'Dark Blue',
|
||||
light_blue: 'Light Blue',
|
||||
dark: 'Very Dark',
|
||||
ayu_dark: 'Ayu Dark',
|
||||
ayu_mirage: 'Ayu Mirage',
|
||||
ayu_light: 'Ayu Light',
|
||||
nord: 'Nord',
|
||||
dracula: 'Dracula',
|
||||
matcha_dark_azul: 'Matcha Dark Azul',
|
||||
qogir_dark: 'Qogir Dark',
|
||||
};
|
||||
|
||||
export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
||||
|
@ -69,14 +69,14 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
|||
...theme,
|
||||
components: {
|
||||
AppShell: {
|
||||
styles: t => ({
|
||||
styles: (t) => ({
|
||||
root: {
|
||||
backgroundColor: t.other.AppShell_backgroundColor,
|
||||
},
|
||||
}),
|
||||
},
|
||||
NavLink: {
|
||||
styles: t => ({
|
||||
styles: (t) => ({
|
||||
icon: {
|
||||
paddingLeft: t.spacing.sm,
|
||||
},
|
||||
|
@ -101,14 +101,14 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
|||
},
|
||||
},
|
||||
Card: {
|
||||
styles: t => ({
|
||||
styles: (t) => ({
|
||||
root: {
|
||||
backgroundColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0],
|
||||
},
|
||||
}),
|
||||
},
|
||||
Image: {
|
||||
styles: t => ({
|
||||
styles: (t) => ({
|
||||
placeholder: {
|
||||
backgroundColor: t.colorScheme === 'dark' ? t.colors.dark[6] : t.colors.gray[0],
|
||||
},
|
||||
|
@ -124,4 +124,4 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
|||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,20 +5,25 @@ import { AudioIcon, FileIcon, PlayIcon } from './icons';
|
|||
|
||||
function Placeholder({ text, Icon, ...props }) {
|
||||
if (props.disableResolve) props.src = null;
|
||||
|
||||
|
||||
return (
|
||||
<Image height={200} withPlaceholder placeholder={
|
||||
<Group>
|
||||
<Icon size={48} />
|
||||
<Text size='md'>{text}</Text>
|
||||
</Group>
|
||||
} {...props} />
|
||||
<Image
|
||||
height={200}
|
||||
withPlaceholder
|
||||
placeholder={
|
||||
<Group>
|
||||
<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 name = (file.name || file.file);
|
||||
const name = file.name || file.file;
|
||||
|
||||
const media = /^(video|audio|image|text)/.test(type);
|
||||
|
||||
|
@ -36,18 +41,34 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
|
|||
}
|
||||
|
||||
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 ? {
|
||||
'video': <video width='100%' autoPlay controls {...props} />,
|
||||
'image': <Image {...props} />,
|
||||
'audio': <audio autoPlay controls {...props} style={{ width: '100%' }}/>,
|
||||
'text': <Prism withLineNumbers language={name.split('.').pop()} {...props} style={{}} sx={{}}>{text}</Prism>,
|
||||
}[type]: <Text>Can'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}/>);
|
||||
};
|
||||
return popup ? (
|
||||
media ? (
|
||||
{
|
||||
video: <video width='100%' autoPlay controls {...props} />,
|
||||
image: <Image {...props} />,
|
||||
audio: <audio autoPlay controls {...props} style={{ width: '100%' }} />,
|
||||
text: (
|
||||
<Prism withLineNumbers language={name.split('.').pop()} {...props} style={{}} sx={{}}>
|
||||
{text}
|
||||
</Prism>
|
||||
),
|
||||
}[type]
|
||||
) : (
|
||||
<Text>Can'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} />
|
||||
);
|
||||
}
|
||||
|
|
|
@ -16,9 +16,7 @@ export default function Dropzone({ loading, onDrop, children }) {
|
|||
</Text>
|
||||
</Group>
|
||||
|
||||
<div style={{ pointerEvents: 'all' }}>
|
||||
{children}
|
||||
</div>
|
||||
<div style={{ pointerEvents: 'all' }}>{children}</div>
|
||||
</MantineDropzone>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,9 +46,7 @@ export default function FileDropzone({ file }: { file: File }) {
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<Badge size='lg'>
|
||||
{file.name}
|
||||
</Badge>
|
||||
<Badge size='lg'>{file.name}</Badge>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Activity } from 'react-feather';
|
|||
|
||||
export default function ActivityIcon({ ...props }) {
|
||||
return <Activity size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Disc } from 'react-feather';
|
|||
|
||||
export default function AudioIcon({ ...props }) {
|
||||
return <Disc size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Calendar } from 'react-feather';
|
|||
|
||||
export default function CalendarIcon({ ...props }) {
|
||||
return <Calendar size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Check } from 'react-feather';
|
|||
|
||||
export default function CheckIcon({ ...props }) {
|
||||
return <Check size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Clock } from 'react-feather';
|
|||
|
||||
export default function ClockIcon({ ...props }) {
|
||||
return <Clock size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Copy } from 'react-feather';
|
|||
|
||||
export default function CopyIcon({ ...props }) {
|
||||
return <Copy size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { X } from 'react-feather';
|
|||
|
||||
export default function CrossIcon({ ...props }) {
|
||||
return <X size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Delete } from 'react-feather';
|
|||
|
||||
export default function DeleteIcon({ ...props }) {
|
||||
return <Delete size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,5 +3,12 @@
|
|||
import Image from 'next/image';
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Download } from 'react-feather';
|
|||
|
||||
export default function DownloadIcon({ ...props }) {
|
||||
return <Download size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { LogIn } from 'react-feather';
|
|||
|
||||
export default function EnterIcon({ ...props }) {
|
||||
return <LogIn size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { ExternalLink } from 'react-feather';
|
|||
|
||||
export default function ExternalLinkIcon({ ...props }) {
|
||||
return <ExternalLink size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Eye } from 'react-feather';
|
|||
|
||||
export default function EyeIcon({ ...props }) {
|
||||
return <Eye size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { File } from 'react-feather';
|
|||
|
||||
export default function FileIcon({ ...props }) {
|
||||
return <File size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,5 +3,12 @@
|
|||
import Image from 'next/image';
|
||||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { GitHub } from 'react-feather';
|
|||
|
||||
export default function GitHubIcon({ ...props }) {
|
||||
return <GitHub size={24} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Hash } from 'react-feather';
|
|||
|
||||
export default function HashIcon({ ...props }) {
|
||||
return <Hash size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Home } from 'react-feather';
|
|||
|
||||
export default function HomeIcon({ ...props }) {
|
||||
return <Home size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Image as FeatherImage } from 'react-feather';
|
|||
|
||||
export default function ImageIcon({ ...props }) {
|
||||
return <FeatherImage size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Link } from 'react-feather';
|
|||
|
||||
export default function LinkIcon({ ...props }) {
|
||||
return <Link size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { LogOut } from 'react-feather';
|
|||
|
||||
export default function LogoutIcon({ ...props }) {
|
||||
return <LogOut size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Edit2 } from 'react-feather';
|
|||
|
||||
export default function PencilIcon({ ...props }) {
|
||||
return <Edit2 size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Play } from 'react-feather';
|
|||
|
||||
export default function PlayIcon({ ...props }) {
|
||||
return <Play size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Plus } from 'react-feather';
|
|||
|
||||
export default function PlusIcon({ ...props }) {
|
||||
return <Plus size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { RefreshCw } from 'react-feather';
|
|||
|
||||
export default function RefreshIcon({ ...props }) {
|
||||
return <RefreshCw size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Settings } from 'react-feather';
|
|||
|
||||
export default function SettingsIcon({ ...props }) {
|
||||
return <Settings size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,4 +4,4 @@ import Image from 'next/image';
|
|||
|
||||
export default function ShareXIcon({ ...props }) {
|
||||
return <Image src='https://getsharex.com/img/ShareX_Logo.svg' width={24} height={24} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Star } from 'react-feather';
|
|||
|
||||
export default function StarIcon({ ...props }) {
|
||||
return <Star size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Tag } from 'react-feather';
|
|||
|
||||
export default function TagIcon({ ...props }) {
|
||||
return <Tag size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Trash2 } from 'react-feather';
|
|||
|
||||
export default function TrashIcon({ ...props }) {
|
||||
return <Trash2 size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Type } from 'react-feather';
|
|||
|
||||
export default function TypeIcon({ ...props }) {
|
||||
return <Type size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Upload } from 'react-feather';
|
|||
|
||||
export default function UploadIcon({ ...props }) {
|
||||
return <Upload size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { User } from 'react-feather';
|
|||
|
||||
export default function UserIcon({ ...props }) {
|
||||
return <User size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,4 +2,4 @@ import { Video } from 'react-feather';
|
|||
|
||||
export default function VideoIcon({ ...props }) {
|
||||
return <Video size={15} {...props} />;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,4 +66,4 @@ export {
|
|||
DiscordIcon,
|
||||
EyeIcon,
|
||||
RefreshIcon,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -12,44 +12,43 @@ export default function RecentFiles({ disableMediaPreview }) {
|
|||
<>
|
||||
<Title>Recent Files</Title>
|
||||
<SimpleGrid
|
||||
cols={(recent.isSuccess && recent.data.length === 0) ? 1 : 4}
|
||||
cols={recent.isSuccess && recent.data.length === 0 ? 1 : 4}
|
||||
spacing='lg'
|
||||
breakpoints={[
|
||||
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
|
||||
]}
|
||||
breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}
|
||||
>
|
||||
{
|
||||
recent.isSuccess
|
||||
? (
|
||||
recent.data.length > 0
|
||||
? (
|
||||
recent.data.map(image => (
|
||||
<File key={randomId()} image={image} updateImages={invalidateFiles} disableMediaPreview={disableMediaPreview} />
|
||||
))
|
||||
) : (
|
||||
<MantineCard shadow='md'>
|
||||
<Center>
|
||||
<Group>
|
||||
<div>
|
||||
<UploadCloud size={48} />
|
||||
</div>
|
||||
<div>
|
||||
<Title>Nothing here</Title>
|
||||
<MutedText size='md'>Upload some files and they will show up here.</MutedText>
|
||||
</div>
|
||||
</Group>
|
||||
</Center>
|
||||
</MantineCard>
|
||||
)
|
||||
) : (
|
||||
[1, 2, 3, 4].map(x => (
|
||||
<div key={x}>
|
||||
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }} />
|
||||
</div>
|
||||
))
|
||||
)
|
||||
}
|
||||
{recent.isSuccess ? (
|
||||
recent.data.length > 0 ? (
|
||||
recent.data.map((image) => (
|
||||
<File
|
||||
key={randomId()}
|
||||
image={image}
|
||||
updateImages={invalidateFiles}
|
||||
disableMediaPreview={disableMediaPreview}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<MantineCard shadow='md'>
|
||||
<Center>
|
||||
<Group>
|
||||
<div>
|
||||
<UploadCloud size={48} />
|
||||
</div>
|
||||
<div>
|
||||
<Title>Nothing here</Title>
|
||||
<MutedText size='md'>Upload some files and they will show up here.</MutedText>
|
||||
</div>
|
||||
</Group>
|
||||
</Center>
|
||||
</MantineCard>
|
||||
)
|
||||
) : (
|
||||
[1, 2, 3, 4].map((x) => (
|
||||
<div key={x}>
|
||||
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }} />
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { useStats } from 'lib/queries/stats';
|
|||
import { Database, Eye, Users } from 'react-feather';
|
||||
|
||||
export function StatCards() {
|
||||
const stats = useStats();
|
||||
const stats = useStats();
|
||||
const latest = stats.data?.[0];
|
||||
const before = stats.data?.[1];
|
||||
|
||||
|
@ -18,44 +18,51 @@ export function StatCards() {
|
|||
{ maxWidth: 'xs', cols: 1 },
|
||||
]}
|
||||
>
|
||||
<StatCard stat={{
|
||||
title: 'UPLOADED FILES',
|
||||
value: stats.isSuccess ? latest.data.count.toLocaleString() : '...',
|
||||
desc: 'files have been uploaded',
|
||||
icon: (
|
||||
<FileIcon />
|
||||
),
|
||||
diff: stats.isSuccess && before?.data ? percentChange(before.data.count, latest.data.count) : undefined,
|
||||
}}/>
|
||||
<StatCard
|
||||
stat={{
|
||||
title: 'UPLOADED FILES',
|
||||
value: stats.isSuccess ? latest.data.count.toLocaleString() : '...',
|
||||
desc: 'files have been uploaded',
|
||||
icon: <FileIcon />,
|
||||
diff:
|
||||
stats.isSuccess && before?.data ? percentChange(before.data.count, latest.data.count) : undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
<StatCard stat={{
|
||||
title: 'STORAGE',
|
||||
value: stats.isSuccess ? latest.data.size : '...',
|
||||
desc: 'of storage used',
|
||||
icon: (
|
||||
<Database size={15} />
|
||||
),
|
||||
diff: stats.isSuccess && before?.data ? percentChange(before.data.size_num, latest.data.size_num) : undefined,
|
||||
}}/>
|
||||
<StatCard
|
||||
stat={{
|
||||
title: 'STORAGE',
|
||||
value: stats.isSuccess ? latest.data.size : '...',
|
||||
desc: 'of storage used',
|
||||
icon: <Database size={15} />,
|
||||
diff:
|
||||
stats.isSuccess && before?.data
|
||||
? percentChange(before.data.size_num, latest.data.size_num)
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
<StatCard stat={{
|
||||
title: 'VIEWS',
|
||||
value: stats.isSuccess ? latest.data.views_count.toLocaleString() : '...',
|
||||
desc: 'total page views',
|
||||
icon: (
|
||||
<Eye size={15} />
|
||||
),
|
||||
diff: stats.isSuccess && before?.data ? percentChange(before.data.views_count, latest.data.views_count) : undefined,
|
||||
}}/>
|
||||
<StatCard
|
||||
stat={{
|
||||
title: 'VIEWS',
|
||||
value: stats.isSuccess ? latest.data.views_count.toLocaleString() : '...',
|
||||
desc: 'total page views',
|
||||
icon: <Eye size={15} />,
|
||||
diff:
|
||||
stats.isSuccess && before?.data
|
||||
? percentChange(before.data.views_count, latest.data.views_count)
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
<StatCard stat={{
|
||||
title: 'USERS',
|
||||
value: stats.isSuccess ? latest.data.count_users.toLocaleString() : '...',
|
||||
desc: 'total registered users',
|
||||
icon: (
|
||||
<Users size={15} />
|
||||
),
|
||||
}}/>
|
||||
<StatCard
|
||||
stat={{
|
||||
title: 'USERS',
|
||||
value: stats.isSuccess ? latest.data.count_users.toLocaleString() : '...',
|
||||
desc: 'total registered users',
|
||||
icon: <Users size={15} />,
|
||||
}}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,7 +29,9 @@ export default function Dashboard({ disableMediaPreview }) {
|
|||
};
|
||||
|
||||
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) {
|
||||
updateImages();
|
||||
showNotification({
|
||||
|
@ -46,7 +48,6 @@ export default function Dashboard({ disableMediaPreview }) {
|
|||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const copyImage = async ({ original }) => {
|
||||
|
@ -65,7 +66,9 @@ export default function Dashboard({ disableMediaPreview }) {
|
|||
return (
|
||||
<div>
|
||||
<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 />
|
||||
|
||||
|
@ -73,7 +76,9 @@ export default function Dashboard({ disableMediaPreview }) {
|
|||
|
||||
<section>
|
||||
<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
|
||||
data={images.data ?? []}
|
||||
loading={images.isLoading}
|
||||
|
@ -124,7 +129,6 @@ export default function Dashboard({ disableMediaPreview }) {
|
|||
},
|
||||
}}
|
||||
empty={<></>}
|
||||
|
||||
columns={[
|
||||
{
|
||||
accessorKey: 'file',
|
||||
|
@ -146,4 +150,4 @@ export default function Dashboard({ disableMediaPreview }) {
|
|||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,35 +29,26 @@ export default function FilePagation({ disableMediaPreview }) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<SimpleGrid
|
||||
cols={3}
|
||||
spacing='lg'
|
||||
breakpoints={[
|
||||
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
|
||||
]}
|
||||
>
|
||||
{
|
||||
(pages.isSuccess)
|
||||
? 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 }}/>
|
||||
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
|
||||
{pages.isSuccess
|
||||
? 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>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
{(pages.isSuccess && pages.data.length) ? (
|
||||
{pages.isSuccess && pages.data.length ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
|
@ -68,10 +59,14 @@ export default function FilePagation({ disableMediaPreview }) {
|
|||
}}
|
||||
>
|
||||
<div></div>
|
||||
<Pagination total={pages.data?.length ?? 0} page={page} onChange={setPage}/>
|
||||
<Checkbox label='Show non-media files' checked={checked} onChange={(event) => setChecked(event.currentTarget.checked)} />
|
||||
<Pagination total={pages.data?.length ?? 0} page={page} onChange={setPage} />
|
||||
<Checkbox
|
||||
label='Show non-media files'
|
||||
checked={checked}
|
||||
onChange={(event) => setChecked(event.currentTarget.checked)}
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,12 +11,12 @@ export default function Files({ disableMediaPreview }) {
|
|||
const favoritePages = usePaginatedFiles({ favorite: 'media' });
|
||||
const [favoritePage, setFavoritePage] = useState(1);
|
||||
|
||||
const updatePages = async favorite => {
|
||||
const updatePages = async (favorite) => {
|
||||
pages.refetch();
|
||||
|
||||
|
||||
if (favorite) {
|
||||
favoritePages.refetch();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -24,50 +24,50 @@ export default function Files({ disableMediaPreview }) {
|
|||
<Group mb='md'>
|
||||
<Title>Files</Title>
|
||||
<Link href='/dashboard/upload' passHref>
|
||||
<ActionIcon component='a' variant='filled' color='primary'><PlusIcon/></ActionIcon>
|
||||
<ActionIcon component='a' variant='filled' color='primary'>
|
||||
<PlusIcon />
|
||||
</ActionIcon>
|
||||
</Link>
|
||||
</Group>
|
||||
{
|
||||
(favoritePages.isSuccess && favoritePages.data.length)
|
||||
? (
|
||||
<Accordion
|
||||
variant='contained'
|
||||
mb='sm'
|
||||
>
|
||||
<Accordion.Item value='favorite'>
|
||||
<Accordion.Control>Favorite Files</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<SimpleGrid
|
||||
cols={3}
|
||||
spacing='lg'
|
||||
breakpoints={[
|
||||
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
|
||||
]}
|
||||
>
|
||||
{(favoritePages.isSuccess && favoritePages.data.length) ? favoritePages.data[(favoritePage - 1) ?? 0].map(image => (
|
||||
{favoritePages.isSuccess && favoritePages.data.length ? (
|
||||
<Accordion variant='contained' mb='sm'>
|
||||
<Accordion.Item value='favorite'>
|
||||
<Accordion.Control>Favorite Files</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
|
||||
{favoritePages.isSuccess && favoritePages.data.length
|
||||
? favoritePages.data[favoritePage - 1 ?? 0].map((image) => (
|
||||
<div key={image.id}>
|
||||
<File image={image} updateImages={() => updatePages(true)} disableMediaPreview={disableMediaPreview} />
|
||||
<File
|
||||
image={image}
|
||||
updateImages={() => updatePages(true)}
|
||||
disableMediaPreview={disableMediaPreview}
|
||||
/>
|
||||
</div>
|
||||
)) : null}
|
||||
</SimpleGrid>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingTop: 12,
|
||||
paddingBottom: 3,
|
||||
}}
|
||||
>
|
||||
<Pagination total={favoritePages.data.length} page={favoritePage} onChange={setFavoritePage} />
|
||||
</Box>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
) : null
|
||||
}
|
||||
|
||||
))
|
||||
: null}
|
||||
</SimpleGrid>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingTop: 12,
|
||||
paddingBottom: 3,
|
||||
}}
|
||||
>
|
||||
<Pagination
|
||||
total={favoritePages.data.length}
|
||||
page={favoritePage}
|
||||
onChange={setFavoritePage}
|
||||
/>
|
||||
</Box>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
) : null}
|
||||
|
||||
<FilePagation disableMediaPreview={disableMediaPreview} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 { useForm } from '@mantine/form';
|
||||
import { useModals } from '@mantine/modals';
|
||||
|
@ -9,17 +22,7 @@ import useFetch from 'hooks/useFetch';
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const expires = [
|
||||
'30m',
|
||||
'1h',
|
||||
'6h',
|
||||
'12h',
|
||||
'1d',
|
||||
'3d',
|
||||
'5d',
|
||||
'7d',
|
||||
'never',
|
||||
];
|
||||
const expires = ['30m', '1h', '6h', '12h', '1d', '3d', '5d', '7d', 'never'];
|
||||
|
||||
function CreateInviteModal({ open, setOpen, updateInvites }) {
|
||||
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 (values.count < 1 || values.count > 100) return form.setFieldError('count', 'Must be between 1 and 100');
|
||||
const expires_at = values.expires === 'never' ? null : new Date({
|
||||
'30m': Date.now() + 30 * 60 * 1000,
|
||||
'1h': Date.now() + 60 * 60 * 1000,
|
||||
'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]);
|
||||
if (values.count < 1 || values.count > 100)
|
||||
return form.setFieldError('count', 'Must be between 1 and 100');
|
||||
const expires_at =
|
||||
values.expires === 'never'
|
||||
? null
|
||||
: new Date(
|
||||
{
|
||||
'30m': Date.now() + 30 * 60 * 1000,
|
||||
'1h': Date.now() + 60 * 60 * 1000,
|
||||
'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);
|
||||
|
||||
|
@ -69,12 +78,8 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
|
|||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={open}
|
||||
onClose={() => setOpen(false)}
|
||||
title={<Title>Create Invite</Title>}
|
||||
>
|
||||
<form onSubmit={form.onSubmit(v => onSubmit(v))}>
|
||||
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>Create Invite</Title>}>
|
||||
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
|
||||
<Select
|
||||
label='Expires'
|
||||
id='expires'
|
||||
|
@ -100,7 +105,7 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
|
|||
min={1}
|
||||
stepHoldDelay={200}
|
||||
stepHoldInterval={100}
|
||||
parser={(v:string) => Number(v.replace(/[^\d]/g, ''))}
|
||||
parser={(v: string) => Number(v.replace(/[^\d]/g, ''))}
|
||||
/>
|
||||
|
||||
<Group position='right' mt={22}>
|
||||
|
@ -120,34 +125,35 @@ export default function Users() {
|
|||
const [invites, setInvites] = useState([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const openDeleteModal = invite => modals.openConfirmModal({
|
||||
title: `Delete ${invite.code}?`,
|
||||
centered: true,
|
||||
overlayBlur: 3,
|
||||
labels: { confirm: 'Yes', cancel: 'No' },
|
||||
onConfirm: async () => {
|
||||
const res = await useFetch(`/api/auth/invite?code=${invite.code}`, 'DELETE');
|
||||
if (res.error) {
|
||||
showNotification({
|
||||
title: 'Failed to delete invite ${invite.code}',
|
||||
message: res.error,
|
||||
icon: <CrossIcon />,
|
||||
color: 'red',
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: `Deleted invite ${invite.code}`,
|
||||
message: '',
|
||||
icon: <DeleteIcon />,
|
||||
color: 'green',
|
||||
});
|
||||
}
|
||||
const openDeleteModal = (invite) =>
|
||||
modals.openConfirmModal({
|
||||
title: `Delete ${invite.code}?`,
|
||||
centered: true,
|
||||
overlayBlur: 3,
|
||||
labels: { confirm: 'Yes', cancel: 'No' },
|
||||
onConfirm: async () => {
|
||||
const res = await useFetch(`/api/auth/invite?code=${invite.code}`, 'DELETE');
|
||||
if (res.error) {
|
||||
showNotification({
|
||||
title: 'Failed to delete invite ${invite.code}',
|
||||
message: res.error,
|
||||
icon: <CrossIcon />,
|
||||
color: 'red',
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: `Deleted invite ${invite.code}`,
|
||||
message: '',
|
||||
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}`);
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
|
@ -162,7 +168,7 @@ export default function Users() {
|
|||
setInvites(us);
|
||||
} else {
|
||||
router.push('/dashboard');
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -174,39 +180,42 @@ export default function Users() {
|
|||
<CreateInviteModal open={open} setOpen={setOpen} updateInvites={updateInvites} />
|
||||
<Group mb='md'>
|
||||
<Title>Invites</Title>
|
||||
<ActionIcon variant='filled' color='primary' onClick={() => setOpen(true)}><PlusIcon /></ActionIcon>
|
||||
<ActionIcon variant='filled' color='primary' onClick={() => setOpen(true)}>
|
||||
<PlusIcon />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
<SimpleGrid
|
||||
cols={3}
|
||||
spacing='lg'
|
||||
breakpoints={[
|
||||
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
|
||||
]}
|
||||
>
|
||||
{invites.length ? invites.map(invite => (
|
||||
<Card key={invite.id} sx={{ maxWidth: '100%' }}>
|
||||
<Group position='apart'>
|
||||
<Group position='left'>
|
||||
<Avatar size='lg' color={invite.used ? 'dark' : 'primary'}>{invite.id}</Avatar>
|
||||
<Stack spacing={0}>
|
||||
<Title>{invite.code}{invite.used && <> (Used)</>}</Title>
|
||||
<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>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Group position='right'>
|
||||
<ActionIcon aria-label='copy' onClick={() => handleCopy(invite)}>
|
||||
<CopyIcon />
|
||||
</ActionIcon>
|
||||
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(invite)}>
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
)) : [1, 2, 3].map(x => (
|
||||
<Skeleton key={x} width='100%' height={100} radius='sm' />
|
||||
))}
|
||||
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
|
||||
{invites.length
|
||||
? invites.map((invite) => (
|
||||
<Card key={invite.id} sx={{ maxWidth: '100%' }}>
|
||||
<Group position='apart'>
|
||||
<Group position='left'>
|
||||
<Avatar size='lg' color={invite.used ? 'dark' : 'primary'}>
|
||||
{invite.id}
|
||||
</Avatar>
|
||||
<Stack spacing={0}>
|
||||
<Title>
|
||||
{invite.code}
|
||||
{invite.used && <> (Used)</>}
|
||||
</Title>
|
||||
<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>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Group position='right'>
|
||||
<ActionIcon aria-label='copy' onClick={() => handleCopy(invite)}>
|
||||
<CopyIcon />
|
||||
</ActionIcon>
|
||||
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(invite)}>
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
))
|
||||
: [1, 2, 3].map((x) => <Skeleton key={x} width='100%' height={100} radius='sm' />)}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { GeneratorModal } from './GeneratorModal';
|
||||
|
||||
export default function Flameshot({ user, open, setOpen }) {
|
||||
const onSubmit = values => {
|
||||
const onSubmit = (values) => {
|
||||
const curl = [
|
||||
'curl',
|
||||
'-H',
|
||||
|
@ -10,7 +10,12 @@ export default function Flameshot({ user, open, setOpen }) {
|
|||
`"authorization: ${user?.token}"`,
|
||||
'-F',
|
||||
'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 = {};
|
||||
|
@ -58,11 +63,13 @@ ${curl.join(' ')} | jq -r '.files[0]' | tr -d '\n' | xsel -ib;
|
|||
pseudoElement.parentNode.removeChild(pseudoElement);
|
||||
};
|
||||
|
||||
return <GeneratorModal
|
||||
opened={open}
|
||||
onClose={() => setOpen(false)}
|
||||
title='Flameshot'
|
||||
desc='To use this script, you need Flameshot, curl, jq, and xsel installed. This script is intended for use on Linux only.'
|
||||
onSubmit={onSubmit}
|
||||
/>;
|
||||
}
|
||||
return (
|
||||
<GeneratorModal
|
||||
opened={open}
|
||||
onClose={() => setOpen(false)}
|
||||
title='Flameshot'
|
||||
desc='To use this script, you need Flameshot, curl, jq, and xsel installed. This script is intended for use on Linux only.'
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,16 +11,11 @@ export function GeneratorModal({ opened, onClose, title, onSubmit, ...other }) {
|
|||
embed: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title={<Title order={3}>{title}</Title>}
|
||||
size='lg'
|
||||
>
|
||||
<Modal opened={opened} onClose={onClose} title={<Title order={3}>{title}</Title>} size='lg'>
|
||||
{other.desc && <Text>{other.desc}</Text>}
|
||||
<form onSubmit={form.onSubmit(values => onSubmit(values))}>
|
||||
<form onSubmit={form.onSubmit((values) => onSubmit(values))}>
|
||||
<Select
|
||||
label='Select file name format'
|
||||
data={[
|
||||
|
@ -34,7 +29,7 @@ export function GeneratorModal({ opened, onClose, title, onSubmit, ...other }) {
|
|||
/>
|
||||
|
||||
<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}
|
||||
min={0}
|
||||
mt='md'
|
||||
|
@ -48,30 +43,19 @@ export function GeneratorModal({ opened, onClose, title, onSubmit, ...other }) {
|
|||
id='zeroWidthSpace'
|
||||
{...form.getInputProps('zeroWidthSpace', { type: 'checkbox' })}
|
||||
/>
|
||||
<Checkbox
|
||||
label='Embed'
|
||||
id='embed'
|
||||
{...form.getInputProps('embed', { type: 'checkbox' })}
|
||||
/>
|
||||
<Checkbox label='Embed' id='embed' {...form.getInputProps('embed', { type: 'checkbox' })} />
|
||||
</Group>
|
||||
|
||||
<Group grow>
|
||||
<Button
|
||||
mt='md'
|
||||
onClick={form.reset}
|
||||
>
|
||||
<Button mt='md' onClick={form.reset}>
|
||||
Reset
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
mt='md'
|
||||
rightIcon={<DownloadIcon />}
|
||||
type='submit'
|
||||
>
|
||||
<Button mt='md' rightIcon={<DownloadIcon />} type='submit'>
|
||||
Download
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,12 @@ export default function ShareX({ user, open, setOpen }) {
|
|||
Name: 'Zipline',
|
||||
DestinationType: 'ImageUploader, TextUploader',
|
||||
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: {
|
||||
Authorization: user?.token,
|
||||
},
|
||||
|
@ -16,7 +21,7 @@ export default function ShareX({ user, open, setOpen }) {
|
|||
FileFormName: 'file',
|
||||
});
|
||||
|
||||
const onSubmit = values => {
|
||||
const onSubmit = (values) => {
|
||||
if (values.format !== 'RANDOM') {
|
||||
config.Headers['Format'] = values.format;
|
||||
setConfig(config);
|
||||
|
@ -50,7 +55,10 @@ export default function ShareX({ user, open, setOpen }) {
|
|||
}
|
||||
|
||||
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.style.display = 'none';
|
||||
document.body.appendChild(pseudoElement);
|
||||
|
@ -58,10 +66,5 @@ export default function ShareX({ user, open, setOpen }) {
|
|||
pseudoElement.parentNode.removeChild(pseudoElement);
|
||||
};
|
||||
|
||||
return <GeneratorModal
|
||||
opened={open}
|
||||
onClose={() => setOpen(false)}
|
||||
title='ShareX'
|
||||
onSubmit={onSubmit}
|
||||
/>;
|
||||
}
|
||||
return <GeneratorModal opened={open} onClose={() => setOpen(false)} title='ShareX' onSubmit={onSubmit} />;
|
||||
}
|
||||
|
|
|
@ -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 { randomId, useInterval } from '@mantine/hooks';
|
||||
import { useModals } from '@mantine/modals';
|
||||
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 Link from 'components/Link';
|
||||
import MutedText from 'components/MutedText';
|
||||
|
@ -17,7 +38,15 @@ import Flameshot from './Flameshot';
|
|||
import ShareX from './ShareX';
|
||||
|
||||
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() {
|
||||
|
@ -70,7 +99,7 @@ export default function Manage() {
|
|||
if (newUser.error) {
|
||||
updateNotification({
|
||||
id: 'update-user',
|
||||
title: 'Couldn\'t save user',
|
||||
title: "Couldn't save user",
|
||||
message: newUser.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
|
@ -96,14 +125,14 @@ export default function Manage() {
|
|||
},
|
||||
});
|
||||
|
||||
const onSubmit = async values => {
|
||||
const onSubmit = async (values) => {
|
||||
const cleanUsername = values.username.trim();
|
||||
const cleanPassword = values.password.trim();
|
||||
const cleanEmbedTitle = values.embedTitle.trim();
|
||||
const cleanEmbedColor = values.embedColor.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({
|
||||
id: 'update-user',
|
||||
|
@ -119,7 +148,10 @@ export default function Manage() {
|
|||
embedTitle: cleanEmbedTitle === '' ? null : cleanEmbedTitle,
|
||||
embedColor: cleanEmbedColor === '' ? null : cleanEmbedColor,
|
||||
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);
|
||||
|
@ -128,22 +160,26 @@ export default function Manage() {
|
|||
if (newUser.invalidDomains) {
|
||||
updateNotification({
|
||||
id: 'update-user',
|
||||
message: <>
|
||||
<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' />
|
||||
</>
|
||||
))}
|
||||
</>,
|
||||
message: (
|
||||
<>
|
||||
<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' />
|
||||
</>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
updateNotification({
|
||||
id: 'update-user',
|
||||
title: 'Couldn\'t save user',
|
||||
title: "Couldn't save user",
|
||||
message: newUser.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
|
@ -164,7 +200,8 @@ export default function Manage() {
|
|||
showNotification({
|
||||
title: 'Export started...',
|
||||
loading: true,
|
||||
message: 'If you have a lot of files, the export may take a while. The list of exports will be updated every 30s.',
|
||||
message:
|
||||
'If you have a lot of files, the export may take a while. The list of exports will be updated every 30s.',
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
|
@ -179,11 +216,15 @@ export default function Manage() {
|
|||
const getExports = async () => {
|
||||
const res = await useFetch('/api/user/export');
|
||||
|
||||
setExports(res.exports.map(s => ({
|
||||
date: new Date(Number(s.name.split('_')[3].slice(0, -4))),
|
||||
size: s.size,
|
||||
full: s.name,
|
||||
})).sort((a, b) => a.date.getTime() - b.date.getTime()));
|
||||
setExports(
|
||||
res.exports
|
||||
.map((s) => ({
|
||||
date: new Date(Number(s.name.split('_')[3].slice(0, -4))),
|
||||
size: s.size,
|
||||
full: s.name,
|
||||
}))
|
||||
.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||
);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
|
@ -193,7 +234,7 @@ export default function Manage() {
|
|||
|
||||
if (!res.count) {
|
||||
showNotification({
|
||||
title: 'Couldn\'t delete files',
|
||||
title: "Couldn't delete files",
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
|
@ -208,24 +249,25 @@ export default function Manage() {
|
|||
}
|
||||
};
|
||||
|
||||
const openDeleteModal = () => modals.openConfirmModal({
|
||||
title: 'Are you sure you want to delete all of your files?',
|
||||
closeOnConfirm: false,
|
||||
labels: { confirm: 'Yes', cancel: 'No' },
|
||||
onConfirm: () => {
|
||||
modals.openConfirmModal({
|
||||
title: 'Are you really sure?',
|
||||
labels: { confirm: 'Yes', cancel: 'No' },
|
||||
onConfirm: () => {
|
||||
handleDelete();
|
||||
modals.closeAll();
|
||||
},
|
||||
onCancel: () => {
|
||||
modals.closeAll();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
const openDeleteModal = () =>
|
||||
modals.openConfirmModal({
|
||||
title: 'Are you sure you want to delete all of your files?',
|
||||
closeOnConfirm: false,
|
||||
labels: { confirm: 'Yes', cancel: 'No' },
|
||||
onConfirm: () => {
|
||||
modals.openConfirmModal({
|
||||
title: 'Are you really sure?',
|
||||
labels: { confirm: 'Yes', cancel: 'No' },
|
||||
onConfirm: () => {
|
||||
handleDelete();
|
||||
modals.closeAll();
|
||||
},
|
||||
onCancel: () => {
|
||||
modals.closeAll();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const interval = useInterval(() => getExports(), 30000);
|
||||
useEffect(() => {
|
||||
|
@ -236,30 +278,49 @@ export default function Manage() {
|
|||
return (
|
||||
<>
|
||||
<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))}>
|
||||
<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')} />
|
||||
<ColorInput id='embedColor' label='Embed Color' {...form.getInputProps('embedColor')} />
|
||||
<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'>
|
||||
<Button
|
||||
type='submit'
|
||||
>Save User</Button>
|
||||
<Button type='submit'>Save User</Button>
|
||||
</Group>
|
||||
</form>
|
||||
|
||||
<Box mb='md'>
|
||||
<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'>
|
||||
<Text>Preview:</Text>
|
||||
<Button
|
||||
leftIcon={fileDataURL ? <Image src={fileDataURL} height={32} radius='md' /> : <SettingsIcon />}
|
||||
sx={t => ({
|
||||
sx={(t) => ({
|
||||
backgroundColor: '#00000000',
|
||||
'&:hover': {
|
||||
backgroundColor: t.other.hover,
|
||||
|
@ -273,8 +334,16 @@ export default function Manage() {
|
|||
</Card>
|
||||
|
||||
<Group position='right' mt='md'>
|
||||
<Button onClick={() => { setFile(null); setFileDataURL(null); }} color='red'>Reset</Button>
|
||||
<Button onClick={saveAvatar} >Save Avatar</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setFile(null);
|
||||
setFileDataURL(null);
|
||||
}}
|
||||
color='red'
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button onClick={saveAvatar}>Save Avatar</Button>
|
||||
</Group>
|
||||
</Box>
|
||||
|
||||
|
@ -284,9 +353,17 @@ export default function Manage() {
|
|||
</Box>
|
||||
|
||||
<Group>
|
||||
<Button onClick={openDeleteModal} rightIcon={<DeleteIcon />} color='red'>Delete All Data</Button>
|
||||
<ExportDataTooltip><Button onClick={exportData} rightIcon={<DownloadIcon />}>Export Data</Button></ExportDataTooltip>
|
||||
<Button onClick={getExports} rightIcon={<RefreshIcon />}>Refresh</Button>
|
||||
<Button onClick={openDeleteModal} rightIcon={<DeleteIcon />} color='red'>
|
||||
Delete All Data
|
||||
</Button>
|
||||
<ExportDataTooltip>
|
||||
<Button onClick={exportData} rightIcon={<DownloadIcon />}>
|
||||
Export Data
|
||||
</Button>
|
||||
</ExportDataTooltip>
|
||||
<Button onClick={getExports} rightIcon={<RefreshIcon />}>
|
||||
Refresh
|
||||
</Button>
|
||||
</Group>
|
||||
<Card mt={22}>
|
||||
{exports && exports.length ? (
|
||||
|
@ -296,11 +373,16 @@ export default function Manage() {
|
|||
{ id: 'date', name: 'Date' },
|
||||
{ id: 'size', name: 'Size' },
|
||||
]}
|
||||
rows={exports ? exports.map((x, i) => ({
|
||||
name: <Link href={'/api/user/export?name=' + x.full}>Export {i + 1}</Link>,
|
||||
date: x.date.toLocaleString(),
|
||||
size: bytesToRead(x.size),
|
||||
})) : []} />
|
||||
rows={
|
||||
exports
|
||||
? exports.map((x, i) => ({
|
||||
name: <Link href={'/api/user/export?name=' + x.full}>Export {i + 1}</Link>,
|
||||
date: x.date.toLocaleString(),
|
||||
size: bytesToRead(x.size),
|
||||
}))
|
||||
: []
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Text>No exports yet</Text>
|
||||
)}
|
||||
|
@ -308,8 +390,12 @@ export default function Manage() {
|
|||
|
||||
<Title my='md'>Uploaders</Title>
|
||||
<Group>
|
||||
<Button size='xl' onClick={() => setShareXOpen(true)} rightIcon={<ShareXIcon />}>Generate ShareX Config</Button>
|
||||
<Button size='xl' onClick={() => setFlameshotOpen(true)} rightIcon={<FlameshotIcon />}>Generate Flameshot Script</Button>
|
||||
<Button size='xl' onClick={() => setShareXOpen(true)} rightIcon={<ShareXIcon />}>
|
||||
Generate ShareX Config
|
||||
</Button>
|
||||
<Button size='xl' onClick={() => setFlameshotOpen(true)} rightIcon={<FlameshotIcon />}>
|
||||
Generate Flameshot Script
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
<ShareX user={user} open={shareXOpen} setOpen={setShareXOpen} />
|
||||
|
|
|
@ -1,5 +1,16 @@
|
|||
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 ColorHash from 'color-hash';
|
||||
import { bytesToRead } from 'lib/utils/client';
|
||||
|
@ -49,13 +60,12 @@ const CHART_OPTIONS = (theme: MantineTheme): ChartOptions => ({
|
|||
},
|
||||
});
|
||||
|
||||
|
||||
type LineChartData = ChartData<'line', number[], string>;
|
||||
type ChartDataMemo = {
|
||||
views: LineChartData,
|
||||
uploads: LineChartData,
|
||||
uploadTypes: ChartData<'pie', number[], string>,
|
||||
storage: LineChartData,
|
||||
views: LineChartData;
|
||||
uploads: LineChartData;
|
||||
uploadTypes: ChartData<'pie', number[], string>;
|
||||
storage: LineChartData;
|
||||
} | void;
|
||||
|
||||
export default function Graphs() {
|
||||
|
@ -77,41 +87,49 @@ export default function Graphs() {
|
|||
return {
|
||||
views: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label: 'Views',
|
||||
data: viewData,
|
||||
borderColor: theme.colors.blue[6],
|
||||
backgroundColor: theme.colors.blue[0],
|
||||
}],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Views',
|
||||
data: viewData,
|
||||
borderColor: theme.colors.blue[6],
|
||||
backgroundColor: theme.colors.blue[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
uploads: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label: 'Uploads',
|
||||
data: uploadData,
|
||||
borderColor: theme.colors.blue[6],
|
||||
backgroundColor: theme.colors.blue[0],
|
||||
}],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Uploads',
|
||||
data: uploadData,
|
||||
borderColor: theme.colors.blue[6],
|
||||
backgroundColor: theme.colors.blue[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
uploadTypes: {
|
||||
labels: latest?.data.types_count.map((x) => x.mimetype),
|
||||
datasets: [{
|
||||
data: latest?.data.types_count.map((x) => x.count),
|
||||
label: 'Upload Types',
|
||||
backgroundColor: latest?.data.types_count.map((x) => hash.hex(x.mimetype)),
|
||||
}],
|
||||
datasets: [
|
||||
{
|
||||
data: latest?.data.types_count.map((x) => x.count),
|
||||
label: 'Upload Types',
|
||||
backgroundColor: latest?.data.types_count.map((x) => hash.hex(x.mimetype)),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
storage: {
|
||||
labels,
|
||||
datasets: [{
|
||||
label: 'Storage',
|
||||
data: storageData,
|
||||
borderColor: theme.colors.blue[6],
|
||||
backgroundColor: theme.colors.blue[0],
|
||||
}],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Storage',
|
||||
data: storageData,
|
||||
borderColor: theme.colors.blue[6],
|
||||
backgroundColor: theme.colors.blue[0],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}, [historicalStats]);
|
||||
|
@ -125,48 +143,43 @@ export default function Graphs() {
|
|||
<Grid.Col md={12} lg={4}>
|
||||
<Card>
|
||||
<Title size='h4'>Upload Types</Title>
|
||||
{
|
||||
chartData && (
|
||||
<Pie
|
||||
data={chartData.uploadTypes}
|
||||
|
||||
options={{
|
||||
plugins: {
|
||||
datalabels: {
|
||||
formatter: (_, ctx) => {
|
||||
// mime: count
|
||||
const mime = ctx.chart.data.labels[ctx.dataIndex];
|
||||
const count = ctx.chart.data.datasets[0].data[ctx.dataIndex];
|
||||
return `${mime}: ${count}`;
|
||||
},
|
||||
|
||||
color: 'white',
|
||||
textShadowBlur: 7,
|
||||
textShadowColor: 'black',
|
||||
{chartData && (
|
||||
<Pie
|
||||
data={chartData.uploadTypes}
|
||||
options={{
|
||||
plugins: {
|
||||
datalabels: {
|
||||
formatter: (_, ctx) => {
|
||||
// mime: count
|
||||
const mime = ctx.chart.data.labels[ctx.dataIndex];
|
||||
const count = ctx.chart.data.datasets[0].data[ctx.dataIndex];
|
||||
return `${mime}: ${count}`;
|
||||
},
|
||||
|
||||
color: 'white',
|
||||
textShadowBlur: 7,
|
||||
textShadowColor: 'black',
|
||||
},
|
||||
}}
|
||||
style={{ maxHeight: '20vh' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
},
|
||||
}}
|
||||
style={{ maxHeight: '20vh' }}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
|
||||
|
||||
{/* 3/4 - views */}
|
||||
<Grid.Col md={12} lg={8}>
|
||||
<Card>
|
||||
<Title size='h4'>Total Views</Title>
|
||||
{
|
||||
chartData && (
|
||||
<Chart
|
||||
type='line'
|
||||
data={chartData.views}
|
||||
options={chartOptions}
|
||||
style={{ maxHeight: '20vh' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{chartData && (
|
||||
<Chart
|
||||
type='line'
|
||||
data={chartData.views}
|
||||
options={chartOptions}
|
||||
style={{ maxHeight: '20vh' }}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
|
||||
|
@ -174,16 +187,14 @@ export default function Graphs() {
|
|||
<Grid.Col md={12} lg={6}>
|
||||
<Card>
|
||||
<Title size='h4'>Total Uploads</Title>
|
||||
{
|
||||
chartData && (
|
||||
<Chart
|
||||
type='line'
|
||||
data={chartData.uploads}
|
||||
options={chartOptions}
|
||||
style={{ maxHeight: '20vh' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{chartData && (
|
||||
<Chart
|
||||
type='line'
|
||||
data={chartData.uploads}
|
||||
options={chartOptions}
|
||||
style={{ maxHeight: '20vh' }}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
|
||||
|
@ -191,46 +202,44 @@ export default function Graphs() {
|
|||
<Grid.Col md={12} lg={6}>
|
||||
<Card>
|
||||
<Title size='h4'>Storage Usage</Title>
|
||||
{
|
||||
chartData && (
|
||||
<Chart
|
||||
type='line'
|
||||
data={chartData.storage}
|
||||
options={{
|
||||
...chartOptions,
|
||||
{chartData && (
|
||||
<Chart
|
||||
type='line'
|
||||
data={chartData.storage}
|
||||
options={{
|
||||
...chartOptions,
|
||||
|
||||
scales: {
|
||||
...chartOptions.scales,
|
||||
y: {
|
||||
...chartOptions.scales.y,
|
||||
scales: {
|
||||
...chartOptions.scales,
|
||||
y: {
|
||||
...chartOptions.scales.y,
|
||||
|
||||
ticks: {
|
||||
callback: (value) => bytesToRead(value as number),
|
||||
color: theme.colors.gray[6],
|
||||
ticks: {
|
||||
callback: (value) => bytesToRead(value as number),
|
||||
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,
|
||||
tooltip: {
|
||||
...chartOptions.plugins.tooltip,
|
||||
callbacks: {
|
||||
label: (context) => {
|
||||
const value = context.raw as number;
|
||||
return bytesToRead(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
style={{ maxHeight: '20vh' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
},
|
||||
}}
|
||||
style={{ maxHeight: '20vh' }}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,8 @@ import { useStats } from 'lib/queries/stats';
|
|||
|
||||
export default function Types() {
|
||||
const stats = useStats();
|
||||
|
||||
if(stats.isLoading) return (
|
||||
<LoadingOverlay visible />
|
||||
);
|
||||
|
||||
if (stats.isLoading) return <LoadingOverlay visible />;
|
||||
|
||||
const latest = stats.data[0];
|
||||
|
||||
|
@ -35,4 +33,4 @@ export default function Types() {
|
|||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,11 +9,10 @@ export default function Stats() {
|
|||
<Title mb='md'>Stats</Title>
|
||||
|
||||
<StatCards />
|
||||
|
||||
|
||||
<Types />
|
||||
|
||||
<Graphs />
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ export default function Upload() {
|
|||
|
||||
useEffect(() => {
|
||||
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();
|
||||
setFiles([...files, file]);
|
||||
showNotification({
|
||||
|
@ -64,35 +64,40 @@ export default function Upload() {
|
|||
});
|
||||
|
||||
const handleUpload = async () => {
|
||||
const expires_at = expires === 'never' ? null : new Date({
|
||||
'5min': Date.now() + 5 * 60 * 1000,
|
||||
'10min': Date.now() + 10 * 60 * 1000,
|
||||
'15min': Date.now() + 15 * 60 * 1000,
|
||||
'30min': Date.now() + 30 * 60 * 1000,
|
||||
'1h': Date.now() + 60 * 60 * 1000,
|
||||
'2h': Date.now() + 2 * 60 * 60 * 1000,
|
||||
'3h': Date.now() + 3 * 60 * 60 * 1000,
|
||||
'4h': Date.now() + 4 * 60 * 60 * 1000,
|
||||
'5h': Date.now() + 5 * 60 * 60 * 1000,
|
||||
'6h': Date.now() + 6 * 60 * 60 * 1000,
|
||||
'8h': Date.now() + 8 * 60 * 60 * 1000,
|
||||
'12h': Date.now() + 12 * 60 * 60 * 1000,
|
||||
'1d': Date.now() + 24 * 60 * 60 * 1000,
|
||||
'3d': Date.now() + 3 * 24 * 60 * 60 * 1000,
|
||||
'5d': Date.now() + 5 * 24 * 60 * 60 * 1000,
|
||||
'7d': Date.now() + 7 * 24 * 60 * 60 * 1000,
|
||||
'1w': Date.now() + 7 * 24 * 60 * 60 * 1000,
|
||||
'1.5w': Date.now() + 1.5 * 7 * 24 * 60 * 60 * 1000,
|
||||
'2w': Date.now() + 2 * 7 * 24 * 60 * 60 * 1000,
|
||||
'3w': Date.now() + 3 * 7 * 24 * 60 * 60 * 1000,
|
||||
'1m': Date.now() + 30 * 24 * 60 * 60 * 1000,
|
||||
'1.5m': Date.now() + 1.5 * 30 * 24 * 60 * 60 * 1000,
|
||||
'2m': Date.now() + 2 * 30 * 24 * 60 * 60 * 1000,
|
||||
'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]);
|
||||
const expires_at =
|
||||
expires === 'never'
|
||||
? null
|
||||
: new Date(
|
||||
{
|
||||
'5min': Date.now() + 5 * 60 * 1000,
|
||||
'10min': Date.now() + 10 * 60 * 1000,
|
||||
'15min': Date.now() + 15 * 60 * 1000,
|
||||
'30min': Date.now() + 30 * 60 * 1000,
|
||||
'1h': Date.now() + 60 * 60 * 1000,
|
||||
'2h': Date.now() + 2 * 60 * 60 * 1000,
|
||||
'3h': Date.now() + 3 * 60 * 60 * 1000,
|
||||
'4h': Date.now() + 4 * 60 * 60 * 1000,
|
||||
'5h': Date.now() + 5 * 60 * 60 * 1000,
|
||||
'6h': Date.now() + 6 * 60 * 60 * 1000,
|
||||
'8h': Date.now() + 8 * 60 * 60 * 1000,
|
||||
'12h': Date.now() + 12 * 60 * 60 * 1000,
|
||||
'1d': Date.now() + 24 * 60 * 60 * 1000,
|
||||
'3d': Date.now() + 3 * 24 * 60 * 60 * 1000,
|
||||
'5d': Date.now() + 5 * 24 * 60 * 60 * 1000,
|
||||
'7d': Date.now() + 7 * 24 * 60 * 60 * 1000,
|
||||
'1w': Date.now() + 7 * 24 * 60 * 60 * 1000,
|
||||
'1.5w': Date.now() + 1.5 * 7 * 24 * 60 * 60 * 1000,
|
||||
'2w': Date.now() + 2 * 7 * 24 * 60 * 60 * 1000,
|
||||
'3w': Date.now() + 3 * 7 * 24 * 60 * 60 * 1000,
|
||||
'1m': Date.now() + 30 * 24 * 60 * 60 * 1000,
|
||||
'1.5m': Date.now() + 1.5 * 30 * 24 * 60 * 60 * 1000,
|
||||
'2m': Date.now() + 2 * 30 * 24 * 60 * 60 * 1000,
|
||||
'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);
|
||||
setLoading(true);
|
||||
|
@ -108,39 +113,53 @@ export default function Upload() {
|
|||
});
|
||||
|
||||
const req = new XMLHttpRequest();
|
||||
req.upload.addEventListener('progress', e => {
|
||||
req.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
setProgress(Math.round(e.loaded / e.total * 100));
|
||||
setProgress(Math.round((e.loaded / e.total) * 100));
|
||||
}
|
||||
});
|
||||
|
||||
req.addEventListener('load', e => {
|
||||
// @ts-ignore not sure why it thinks response doesnt exist, but it does.
|
||||
const json = JSON.parse(e.target.response);
|
||||
setLoading(false);
|
||||
req.addEventListener(
|
||||
'load',
|
||||
(e) => {
|
||||
// @ts-ignore not sure why it thinks response doesnt exist, but it does.
|
||||
const json = JSON.parse(e.target.response);
|
||||
setLoading(false);
|
||||
|
||||
if (json.error === undefined) {
|
||||
updateNotification({
|
||||
id: 'upload',
|
||||
title: 'Upload Successful',
|
||||
message: <>Copied first file to clipboard! <br />{json.files.map(x => (<Link key={x} href={x}>{x}<br /></Link>))}</>,
|
||||
color: 'green',
|
||||
icon: <UploadIcon />,
|
||||
});
|
||||
clipboard.copy(json.files[0]);
|
||||
setFiles([]);
|
||||
invalidateFiles();
|
||||
} else {
|
||||
updateNotification({
|
||||
id: 'upload',
|
||||
title: 'Upload Failed',
|
||||
message: json.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
setProgress(0);
|
||||
}, false);
|
||||
if (json.error === undefined) {
|
||||
updateNotification({
|
||||
id: 'upload',
|
||||
title: 'Upload Successful',
|
||||
message: (
|
||||
<>
|
||||
Copied first file to clipboard! <br />
|
||||
{json.files.map((x) => (
|
||||
<Link key={x} href={x}>
|
||||
{x}
|
||||
<br />
|
||||
</Link>
|
||||
))}
|
||||
</>
|
||||
),
|
||||
color: 'green',
|
||||
icon: <UploadIcon />,
|
||||
});
|
||||
clipboard.copy(json.files[0]);
|
||||
setFiles([]);
|
||||
invalidateFiles();
|
||||
} else {
|
||||
updateNotification({
|
||||
id: 'upload',
|
||||
title: 'Upload Failed',
|
||||
message: json.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
setProgress(0);
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
req.open('POST', '/api/upload');
|
||||
req.setRequestHeader('Authorization', user.token);
|
||||
|
@ -156,7 +175,9 @@ export default function Upload() {
|
|||
|
||||
<Dropzone loading={loading} onDrop={(f) => setFiles([...files, ...f])}>
|
||||
<Group position='center' spacing='md'>
|
||||
{files.map(file => (<FileDropzone key={randomId()} file={file} />))}
|
||||
{files.map((file) => (
|
||||
<FileDropzone key={randomId()} file={file} />
|
||||
))}
|
||||
</Group>
|
||||
</Dropzone>
|
||||
|
||||
|
@ -167,7 +188,7 @@ export default function Upload() {
|
|||
<Group position='right' mt='md'>
|
||||
<Tooltip label='Add a password to your files (optional, leave blank for none)'>
|
||||
<PasswordInput
|
||||
style={{width: '252px'}}
|
||||
style={{ width: '252px' }}
|
||||
placeholder='Password'
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.currentTarget.value)}
|
||||
|
@ -210,7 +231,9 @@ export default function Upload() {
|
|||
]}
|
||||
/>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -26,7 +26,7 @@ export default function Upload() {
|
|||
});
|
||||
|
||||
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.
|
||||
const json = JSON.parse(e.target.response);
|
||||
|
||||
|
@ -34,7 +34,17 @@ export default function Upload() {
|
|||
updateNotification({
|
||||
id: 'upload-text',
|
||||
title: 'Upload Successful',
|
||||
message: <>Copied first file to clipboard! <br />{json.files.map(x => (<Link key={x} href={x}>{x}<br /></Link>))}</>,
|
||||
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>
|
||||
|
||||
<CodeInput
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
/>
|
||||
<CodeInput value={value} onChange={(e) => setValue(e.target.value)} />
|
||||
|
||||
<Group position='right' mt='md'>
|
||||
<Select
|
||||
value={lang}
|
||||
onChange={setLang}
|
||||
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 />}
|
||||
/>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -5,13 +5,11 @@ import { CopyIcon, CrossIcon, DeleteIcon, ExternalLinkIcon } from 'components/ic
|
|||
import TrashIcon from 'components/icons/TrashIcon';
|
||||
import { URLResponse, useURLDelete } from 'lib/queries/url';
|
||||
|
||||
export default function URLCard({ url }: {
|
||||
url: URLResponse
|
||||
}) {
|
||||
export default function URLCard({ url }: { url: URLResponse }) {
|
||||
const clipboard = useClipboard();
|
||||
const urlDelete = useURLDelete();
|
||||
|
||||
const copyURL = u => {
|
||||
|
||||
const copyURL = (u) => {
|
||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${u.url}`);
|
||||
showNotification({
|
||||
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, {
|
||||
onSuccess: () => {
|
||||
showNotification({
|
||||
|
@ -44,7 +42,7 @@ export default function URLCard({ url }: {
|
|||
|
||||
return (
|
||||
<Card key={url.id} sx={{ maxWidth: '100%' }} shadow='sm'>
|
||||
<LoadingOverlay visible={urlDelete.isLoading}/>
|
||||
<LoadingOverlay visible={urlDelete.isLoading} />
|
||||
|
||||
<Group position='apart'>
|
||||
<Group position='left'>
|
||||
|
@ -64,4 +62,4 @@ export default function URLCard({ url }: {
|
|||
</Group>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 { showNotification } from '@mantine/notifications';
|
||||
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 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 {
|
||||
new URL(cleanURL);
|
||||
|
@ -45,7 +56,7 @@ export default function Urls() {
|
|||
const res = await fetch('/api/shorten', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': user.token,
|
||||
Authorization: user.token,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
|
@ -77,11 +88,7 @@ export default function Urls() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
opened={createOpen}
|
||||
onClose={() => setCreateOpen(false)}
|
||||
title={<Title>Shorten URL</Title>}
|
||||
>
|
||||
<Modal opened={createOpen} onClose={() => setCreateOpen(false)} title={<Title>Shorten URL</Title>}>
|
||||
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
|
||||
<TextInput id='url' label='URL' {...form.getInputProps('url')} />
|
||||
<TextInput id='vanity' label='Vanity' {...form.getInputProps('vanity')} />
|
||||
|
@ -95,44 +102,32 @@ export default function Urls() {
|
|||
|
||||
<Group mb='md'>
|
||||
<Title>URLs</Title>
|
||||
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}><PlusIcon /></ActionIcon>
|
||||
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}>
|
||||
<PlusIcon />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
|
||||
{
|
||||
(urls.data && urls.data.length === 0) && (
|
||||
<Card shadow='md'>
|
||||
<Center>
|
||||
<Group>
|
||||
<div>
|
||||
<LinkIcon size={48} />
|
||||
</div>
|
||||
<div>
|
||||
<Title>Nothing here</Title>
|
||||
<MutedText size='md'>Create a link to get started!</MutedText>
|
||||
</div>
|
||||
</Group>
|
||||
</Center>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
{urls.data && urls.data.length === 0 && (
|
||||
<Card shadow='md'>
|
||||
<Center>
|
||||
<Group>
|
||||
<div>
|
||||
<LinkIcon size={48} />
|
||||
</div>
|
||||
<div>
|
||||
<Title>Nothing here</Title>
|
||||
<MutedText size='md'>Create a link to get started!</MutedText>
|
||||
</div>
|
||||
</Group>
|
||||
</Center>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<SimpleGrid
|
||||
cols={4}
|
||||
spacing='lg'
|
||||
breakpoints={[
|
||||
{ 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 cols={4} spacing='lg' breakpoints={[{ 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,11 +13,11 @@ export function CreateUserModal({ open, setOpen, updateUsers }) {
|
|||
},
|
||||
});
|
||||
|
||||
const onSubmit = async values => {
|
||||
const onSubmit = async (values) => {
|
||||
const cleanUsername = values.username.trim();
|
||||
const cleanPassword = values.password.trim();
|
||||
if (cleanUsername === '') return form.setFieldError('username', 'Username can\'t be nothing');
|
||||
if (cleanPassword === '') return form.setFieldError('password', 'Password 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");
|
||||
|
||||
const data = {
|
||||
username: cleanUsername,
|
||||
|
@ -47,12 +47,8 @@ export function CreateUserModal({ open, setOpen, updateUsers }) {
|
|||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={open}
|
||||
onClose={() => setOpen(false)}
|
||||
title={<Title>Create User</Title>}
|
||||
>
|
||||
<form onSubmit={form.onSubmit(v => onSubmit(v))}>
|
||||
<Modal opened={open} 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='password' label='Password' type='password' {...form.getInputProps('password')} />
|
||||
<Switch mt={12} id='administrator' label='Administrator' {...form.getInputProps('administrator')} />
|
||||
|
@ -64,4 +60,4 @@ export function CreateUserModal({ open, setOpen, updateUsers }) {
|
|||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,16 +6,17 @@ import useFetch from 'hooks/useFetch';
|
|||
|
||||
export function EditUserModal({ open, setOpen, updateUsers, user }) {
|
||||
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 cleanPassword = values.password.trim();
|
||||
|
||||
|
@ -28,7 +29,6 @@ export function EditUserModal({ open, setOpen, updateUsers, user }) {
|
|||
if (cleanUsername !== '' && cleanUsername !== user.username) data.username = cleanUsername;
|
||||
if (cleanPassword !== '') data.password = cleanPassword;
|
||||
|
||||
|
||||
setOpen(false);
|
||||
const res = await useFetch('/api/user/' + user.id, 'PATCH', data);
|
||||
if (res.error) {
|
||||
|
@ -51,13 +51,9 @@ export function EditUserModal({ open, setOpen, updateUsers, user }) {
|
|||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={open}
|
||||
onClose={() => setOpen(false)}
|
||||
title={<Title>Edit User {user?.username}</Title>}
|
||||
>
|
||||
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>Edit User {user?.username}</Title>}>
|
||||
{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='password' label='Password' type='password' {...form.getInputProps('password')} />
|
||||
<Switch mt={12} id='administrator' label='Administrator' {...form.getInputProps('administrator')} />
|
||||
|
|
|
@ -45,27 +45,28 @@ export default function Users() {
|
|||
};
|
||||
|
||||
// 2-step modal for deleting user if they want to delete their images too.
|
||||
const openDeleteModal = user => modals.openConfirmModal({
|
||||
title: `Delete ${user.username}?`,
|
||||
closeOnConfirm: false,
|
||||
labels: { confirm: 'Yes', cancel: 'No' },
|
||||
onConfirm: () => {
|
||||
modals.openConfirmModal({
|
||||
title: `Delete ${user.username}'s images?`,
|
||||
labels: { confirm: 'Yes', cancel: 'No' },
|
||||
centered: true,
|
||||
overlayBlur: 3,
|
||||
onConfirm: () => {
|
||||
handleDelete(user, true);
|
||||
modals.closeAll();
|
||||
},
|
||||
onCancel: () => {
|
||||
handleDelete(user, false);
|
||||
modals.closeAll();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
const openDeleteModal = (user) =>
|
||||
modals.openConfirmModal({
|
||||
title: `Delete ${user.username}?`,
|
||||
closeOnConfirm: false,
|
||||
labels: { confirm: 'Yes', cancel: 'No' },
|
||||
onConfirm: () => {
|
||||
modals.openConfirmModal({
|
||||
title: `Delete ${user.username}'s images?`,
|
||||
labels: { confirm: 'Yes', cancel: 'No' },
|
||||
centered: true,
|
||||
overlayBlur: 3,
|
||||
onConfirm: () => {
|
||||
handleDelete(user, true);
|
||||
modals.closeAll();
|
||||
},
|
||||
onCancel: () => {
|
||||
handleDelete(user, false);
|
||||
modals.closeAll();
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const updateUsers = async () => {
|
||||
const us = await useFetch('/api/users');
|
||||
|
@ -73,7 +74,7 @@ export default function Users() {
|
|||
setUsers(us);
|
||||
} else {
|
||||
router.push('/dashboard');
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -84,48 +85,57 @@ export default function Users() {
|
|||
<>
|
||||
<CreateUserModal open={createOpen} setOpen={setCreateOpen} updateUsers={updateUsers} />
|
||||
<EditUserModal open={editOpen} setOpen={setEditOpen} updateUsers={updateUsers} user={selectedUser} />
|
||||
|
||||
|
||||
<Group mb='md'>
|
||||
<Title>Users</Title>
|
||||
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}><PlusIcon /></ActionIcon>
|
||||
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}>
|
||||
<PlusIcon />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
<SimpleGrid
|
||||
cols={3}
|
||||
spacing='lg'
|
||||
breakpoints={[
|
||||
{ maxWidth: 'sm', cols: 1, spacing: 'sm' },
|
||||
]}
|
||||
>
|
||||
{users.length ? users.filter(x => x.username !== user.username).map(user => (
|
||||
<Card key={user.id} sx={{ maxWidth: '100%' }}>
|
||||
<Group position='apart'>
|
||||
<Group position='left'>
|
||||
<Avatar size='lg' color={user.administrator ? 'primary' : 'dark'} src={user.avatar ?? null}>{user.username[0]}</Avatar>
|
||||
<Stack spacing={0}>
|
||||
<Title>{user.username}</Title>
|
||||
<MutedText size='sm'>ID: {user.id}</MutedText>
|
||||
<MutedText size='sm'>Administrator: {user.administrator ? 'yes' : 'no'}</MutedText>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Group position='right'>
|
||||
{user.administrator ? null : (
|
||||
<>
|
||||
<ActionIcon aria-label='edit' onClick={() => {setEditOpen(true); setSelectedUser(user);}}>
|
||||
<PencilIcon />
|
||||
</ActionIcon>
|
||||
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(user)}>
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</>
|
||||
)}
|
||||
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
)) : [1, 2, 3].map(x => (
|
||||
<Skeleton key={x} width='100%' height={100} radius='sm' />
|
||||
))}
|
||||
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
|
||||
{users.length
|
||||
? users
|
||||
.filter((x) => x.username !== user.username)
|
||||
.map((user) => (
|
||||
<Card key={user.id} sx={{ maxWidth: '100%' }}>
|
||||
<Group position='apart'>
|
||||
<Group position='left'>
|
||||
<Avatar
|
||||
size='lg'
|
||||
color={user.administrator ? 'primary' : 'dark'}
|
||||
src={user.avatar ?? null}
|
||||
>
|
||||
{user.username[0]}
|
||||
</Avatar>
|
||||
<Stack spacing={0}>
|
||||
<Title>{user.username}</Title>
|
||||
<MutedText size='sm'>ID: {user.id}</MutedText>
|
||||
<MutedText size='sm'>Administrator: {user.administrator ? 'yes' : 'no'}</MutedText>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Group position='right'>
|
||||
{user.administrator ? null : (
|
||||
<>
|
||||
<ActionIcon
|
||||
aria-label='edit'
|
||||
onClick={() => {
|
||||
setEditOpen(true);
|
||||
setSelectedUser(user);
|
||||
}}
|
||||
>
|
||||
<PencilIcon />
|
||||
</ActionIcon>
|
||||
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(user)}>
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</>
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
))
|
||||
: [1, 2, 3].map((x) => <Skeleton key={x} width='100%' height={100} radius='sm' />)}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,4 +4,4 @@ import validateConfig from './config/validateConfig';
|
|||
|
||||
if (!global.config) global.config = validateConfig(readConfig());
|
||||
|
||||
export default global.config as Config;
|
||||
export default global.config as Config;
|
||||
|
|
|
@ -5,7 +5,7 @@ export interface ConfigCore {
|
|||
port: number;
|
||||
database_url: string;
|
||||
logger: boolean;
|
||||
|
||||
|
||||
stats_interval: number;
|
||||
invites_interval: number;
|
||||
}
|
||||
|
@ -121,4 +121,4 @@ export interface Config {
|
|||
discord: ConfigDiscord;
|
||||
oauth: ConfigOAuth;
|
||||
features: ConfigFeatures;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,7 +63,6 @@ export default function readConfig() {
|
|||
map('DATASOURCE_S3_REGION', 'string', 'datasource.s3.region'),
|
||||
map('DATASOURCE_S3_USE_SSL', 'boolean', 'datasource.s3.use_ssl'),
|
||||
|
||||
|
||||
map('DATASOURCE_SWIFT_USERNAME', 'string', 'datasource.swift.username'),
|
||||
map('DATASOURCE_SWIFT_PASSWORD', 'string', 'datasource.swift.password'),
|
||||
map('DATASOURCE_SWIFT_AUTH_ENDPOINT', 'string', 'datasource.swift.auth_endpoint'),
|
||||
|
@ -152,10 +151,10 @@ export default function readConfig() {
|
|||
default:
|
||||
parsed = value;
|
||||
break;
|
||||
};
|
||||
}
|
||||
|
||||
set(config, map.path, parsed);
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,18 +3,22 @@ import { CombinedError, s, ValidationError } from '@sapphire/shapeshift';
|
|||
import { inspect } from 'util';
|
||||
import Logger from 'lib/logger';
|
||||
|
||||
const discord_content = s.object({
|
||||
content: s.string.nullish.default(null),
|
||||
embed: s.object({
|
||||
title: s.string.nullish.default(null),
|
||||
description: s.string.nullish.default(null),
|
||||
footer: s.string.nullish.default(null),
|
||||
color: s.number.notEqual(NaN).nullish.default(null),
|
||||
thumbnail: s.boolean.default(false),
|
||||
image: s.boolean.default(true),
|
||||
timestamp: s.boolean.default(true),
|
||||
}).default(null),
|
||||
}).default(null);
|
||||
const discord_content = s
|
||||
.object({
|
||||
content: s.string.nullish.default(null),
|
||||
embed: s
|
||||
.object({
|
||||
title: s.string.nullish.default(null),
|
||||
description: s.string.nullish.default(null),
|
||||
footer: s.string.nullish.default(null),
|
||||
color: s.number.notEqual(NaN).nullish.default(null),
|
||||
thumbnail: s.boolean.default(false),
|
||||
image: s.boolean.default(true),
|
||||
timestamp: s.boolean.default(true),
|
||||
})
|
||||
.default(null),
|
||||
})
|
||||
.default(null);
|
||||
|
||||
const validator = s.object({
|
||||
core: s.object({
|
||||
|
@ -27,117 +31,139 @@ const validator = s.object({
|
|||
stats_interval: s.number.default(1800),
|
||||
invites_interval: s.number.default(1800),
|
||||
}),
|
||||
datasource: s.object({
|
||||
type: s.enum('local', 's3', 'swift').default('local'),
|
||||
local: s.object({
|
||||
directory: s.string.default('./uploads'),
|
||||
}).default({
|
||||
directory: './uploads',
|
||||
datasource: s
|
||||
.object({
|
||||
type: s.enum('local', 's3', 'swift').default('local'),
|
||||
local: s
|
||||
.object({
|
||||
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({
|
||||
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',
|
||||
},
|
||||
}),
|
||||
uploader: s.object({
|
||||
route: s.string.default('/u'),
|
||||
embed_route: s.string.default('/a'),
|
||||
length: s.number.default(6),
|
||||
admin_limit: s.number.default(104900000),
|
||||
user_limit: s.number.default(104900000),
|
||||
disabled_extensions: s.string.array.default([]),
|
||||
format_date: s.string.default('YYYY-MM-DD_HH:mm:ss'),
|
||||
}).default({
|
||||
route: '/u',
|
||||
embed_route: '/a',
|
||||
length: 6,
|
||||
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),
|
||||
uploader: s
|
||||
.object({
|
||||
route: s.string.default('/u'),
|
||||
embed_route: s.string.default('/a'),
|
||||
length: s.number.default(6),
|
||||
admin_limit: s.number.default(104900000),
|
||||
user_limit: s.number.default(104900000),
|
||||
disabled_extensions: s.string.array.default([]),
|
||||
format_date: s.string.default('YYYY-MM-DD_HH:mm:ss'),
|
||||
})
|
||||
.default({
|
||||
route: '/u',
|
||||
embed_route: '/a',
|
||||
length: 6,
|
||||
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({
|
||||
label: s.string,
|
||||
link: s.string,
|
||||
})).default([
|
||||
{ label: 'Zipline', link: 'https://github.com/diced/zipline' },
|
||||
{ label: 'Documentation', link: 'https://zipline.diced.tech/' },
|
||||
]),
|
||||
}).default({
|
||||
title: 'Zipline',
|
||||
show_files_per_user: true,
|
||||
show_version: true,
|
||||
disable_media_preview: false,
|
||||
external_links: s
|
||||
.array(
|
||||
s.object({
|
||||
label: s.string,
|
||||
link: s.string,
|
||||
})
|
||||
)
|
||||
.default([
|
||||
{ label: 'Zipline', link: 'https://github.com/diced/zipline' },
|
||||
{ label: 'Documentation', link: 'https://zipline.diced.tech/' },
|
||||
]),
|
||||
})
|
||||
.default({
|
||||
title: 'Zipline',
|
||||
show_files_per_user: true,
|
||||
show_version: true,
|
||||
disable_media_preview: false,
|
||||
|
||||
external_links: [
|
||||
{ label: 'Zipline', link: 'https://github.com/diced/zipline' },
|
||||
{ label: 'Documentation', link: 'https://zipline.diced.tech/' },
|
||||
],
|
||||
}),
|
||||
external_links: [
|
||||
{ label: 'Zipline', link: 'https://github.com/diced/zipline' },
|
||||
{ label: 'Documentation', link: 'https://zipline.diced.tech/' },
|
||||
],
|
||||
}),
|
||||
discord: s.object({
|
||||
url: s.string,
|
||||
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,
|
||||
shorten: discord_content,
|
||||
}),
|
||||
oauth: s.object({
|
||||
github_client_id: s.string.nullable.default(null),
|
||||
github_client_secret: s.string.nullable.default(null),
|
||||
oauth: s
|
||||
.object({
|
||||
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_secret: s.string.nullable.default(null),
|
||||
}).nullish.default(null),
|
||||
features: s.object({
|
||||
invites: s.boolean.default(true),
|
||||
oauth_registration: s.boolean.default(false),
|
||||
}).default({ invites: true, oauth_registration: false }),
|
||||
discord_client_id: s.string.nullable.default(null),
|
||||
discord_client_secret: s.string.nullable.default(null),
|
||||
})
|
||||
.nullish.default(null),
|
||||
features: s
|
||||
.object({
|
||||
invites: s.boolean.default(true),
|
||||
oauth_registration: s.boolean.default(false),
|
||||
})
|
||||
.default({ invites: true, oauth_registration: false }),
|
||||
});
|
||||
|
||||
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');
|
||||
if (!validated.datasource.s3.secret_access_key)
|
||||
errors.push('datasource.s3.secret_access_key is a required field');
|
||||
if (!validated.datasource.s3.bucket)
|
||||
errors.push('datasource.s3.bucket is a required field');
|
||||
if (!validated.datasource.s3.endpoint)
|
||||
errors.push('datasource.s3.endpoint is a required field');
|
||||
if (!validated.datasource.s3.bucket) errors.push('datasource.s3.bucket is a required field');
|
||||
if (!validated.datasource.s3.endpoint) errors.push('datasource.s3.endpoint is a required field');
|
||||
if (errors.length) throw { errors };
|
||||
break;
|
||||
}
|
||||
|
@ -185,4 +209,4 @@ export default function validate(config): Config {
|
|||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,4 +21,4 @@ if (!global.datasource) {
|
|||
}
|
||||
}
|
||||
|
||||
export default global.datasource;
|
||||
export default global.datasource;
|
||||
|
|
|
@ -8,4 +8,4 @@ export abstract class Datasource {
|
|||
public abstract size(file: string): Promise<number>;
|
||||
public abstract get(file: string): Readable | Promise<Readable>;
|
||||
public abstract fullSize(): Promise<number>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,19 +31,19 @@ export class Local extends Datasource {
|
|||
|
||||
public async size(file: string): Promise<number> {
|
||||
const stats = await stat(join(process.cwd(), this.path, file));
|
||||
|
||||
|
||||
return stats.size;
|
||||
}
|
||||
|
||||
public async fullSize(): Promise<number> {
|
||||
const files = await readdir(this.path);
|
||||
|
||||
|
||||
let size = 0;
|
||||
for (let i = 0, L = files.length; i !== L; ++i) {
|
||||
const sta = await stat(join(this.path, files[i]));
|
||||
size += sta.size;
|
||||
}
|
||||
|
||||
|
||||
return size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,9 +7,7 @@ export class S3 extends Datasource {
|
|||
public name: string = 'S3';
|
||||
public s3: Client;
|
||||
|
||||
public constructor(
|
||||
public config: ConfigS3Datasource,
|
||||
) {
|
||||
public constructor(public config: ConfigS3Datasource) {
|
||||
super();
|
||||
this.s3 = new Client({
|
||||
endPoint: config.endpoint,
|
||||
|
@ -55,11 +53,11 @@ export class S3 extends Datasource {
|
|||
const objects = this.s3.listObjectsV2(this.config.bucket, '', true);
|
||||
let size = 0;
|
||||
|
||||
objects.on('data', item => size += item.size);
|
||||
objects.on('end', err => {
|
||||
objects.on('data', (item) => (size += item.size));
|
||||
objects.on('end', (err) => {
|
||||
if (err) rej(err);
|
||||
else res(size);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,9 +44,7 @@ class SwiftContainer {
|
|||
const endpoint = catalogEntry.endpoints.find(
|
||||
(x: any) =>
|
||||
x.interface === (this.options.credentials.interface || 'public') &&
|
||||
(this.options.credentials.region_id
|
||||
? x.region_id == this.options.credentials.region_id
|
||||
: true)
|
||||
(this.options.credentials.region_id ? x.region_id == this.options.credentials.region_id : true)
|
||||
);
|
||||
|
||||
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;
|
||||
// many Swift clouds use ceph radosgw to provide swift
|
||||
|
@ -124,10 +123,15 @@ class SwiftContainer {
|
|||
|
||||
public async listObjects(query?: string): Promise<SwiftObject[]> {
|
||||
const auth = await this.authenticate();
|
||||
return await fetch(`${auth.swiftURL}/${this.options.credentials.container}${query ? `${query.startsWith('?') ? '' : '?'}${query}` : ''}`, {
|
||||
method: 'GET',
|
||||
headers: this.generateHeaders(auth.token),
|
||||
}).then((e) => e.json());
|
||||
return await fetch(
|
||||
`${auth.swiftURL}/${this.options.credentials.container}${
|
||||
query ? `${query.startsWith('?') ? '' : '?'}${query}` : ''
|
||||
}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: this.generateHeaders(auth.token),
|
||||
}
|
||||
).then((e) => e.json());
|
||||
}
|
||||
|
||||
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> {
|
||||
try {
|
||||
const head = await this.container.headObject(file);
|
||||
|
||||
|
||||
return head.headers.get('content-length') || 0;
|
||||
} catch {
|
||||
return 0;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export { Datasource } from './Datasource';
|
||||
export { Local } from './Local';
|
||||
export { S3 } from './S3';
|
||||
export { Swift } from './Swift';
|
||||
export { Swift } from './Swift';
|
||||
|
|
|
@ -15,37 +15,44 @@ function parse(str: string, args: Args) {
|
|||
.replace(/{user\.name}/gi, args[0].username)
|
||||
.replace(/{link}/gi, args[3]);
|
||||
|
||||
if (args[1]) str = str
|
||||
.replace(/{file\.id}/gi, args[1].id.toString())
|
||||
.replace(/{file\.mime}/gi, args[1].mimetype)
|
||||
.replace(/{file\.file}/gi, args[1].file)
|
||||
.replace(/{file\.created_at.full_string}/gi, args[1].created_at.toLocaleString())
|
||||
.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[1])
|
||||
str = str
|
||||
.replace(/{file\.id}/gi, args[1].id.toString())
|
||||
.replace(/{file\.mime}/gi, args[1].mimetype)
|
||||
.replace(/{file\.file}/gi, args[1].file)
|
||||
.replace(/{file\.created_at.full_string}/gi, args[1].created_at.toLocaleString())
|
||||
.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
|
||||
.replace(/{url\.id}/gi, args[2].id.toString())
|
||||
.replace(/{url\.vanity}/gi, args[2].vanity ? args[2].vanity : 'none')
|
||||
.replace(/{url\.destination}/gi, args[2].destination)
|
||||
.replace(/{url\.created_at.full_string}/gi, args[2].created_at.toLocaleString())
|
||||
.replace(/{url\.created_at.time_string}/gi, args[2].created_at.toLocaleTimeString())
|
||||
.replace(/{url\.created_at.date_string}/gi, args[2].created_at.toLocaleDateString());
|
||||
if (args[2])
|
||||
str = str
|
||||
.replace(/{url\.id}/gi, args[2].id.toString())
|
||||
.replace(/{url\.vanity}/gi, args[2].vanity ? args[2].vanity : 'none')
|
||||
.replace(/{url\.destination}/gi, args[2].destination)
|
||||
.replace(/{url\.created_at.full_string}/gi, args[2].created_at.toLocaleString())
|
||||
.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;
|
||||
}
|
||||
|
||||
export function parseContent(content: ConfigDiscordContent, args: Args): ConfigDiscordContent & { url: string } {
|
||||
export function parseContent(
|
||||
content: ConfigDiscordContent,
|
||||
args: Args
|
||||
): ConfigDiscordContent & { url: string } {
|
||||
return {
|
||||
content: parse(content.content, args),
|
||||
embed: content.embed ? {
|
||||
title: parse(content.embed.title, args),
|
||||
description: parse(content.embed.description, args),
|
||||
footer: parse(content.embed.footer, args),
|
||||
color: content.embed.color,
|
||||
thumbnail: content.embed.thumbnail,
|
||||
timestamp: content.embed.timestamp,
|
||||
image: content.embed.image,
|
||||
} : null,
|
||||
embed: content.embed
|
||||
? {
|
||||
title: parse(content.embed.title, args),
|
||||
description: parse(content.embed.description, args),
|
||||
footer: parse(content.embed.footer, args),
|
||||
color: content.embed.color,
|
||||
thumbnail: content.embed.thumbnail,
|
||||
timestamp: content.embed.timestamp,
|
||||
image: content.embed.image,
|
||||
}
|
||||
: null,
|
||||
url: args[3],
|
||||
};
|
||||
}
|
||||
|
@ -60,22 +67,34 @@ export async function sendUpload(user: User, image: Image, host: string) {
|
|||
username: config.discord.username,
|
||||
avatar_url: config.discord.avatar_url,
|
||||
content: parsed.content ?? null,
|
||||
embeds: parsed.embed ? [{
|
||||
title: parsed.embed.title ?? null,
|
||||
description: parsed.embed.description ?? null,
|
||||
url: parsed.url ?? null,
|
||||
timestamp: parsed.embed.timestamp ? image.created_at.toISOString() : null,
|
||||
color: parsed.embed.color ?? null,
|
||||
footer: parsed.embed.footer ? {
|
||||
text: parsed.embed.footer,
|
||||
} : null,
|
||||
thumbnail: isImage && parsed.embed.thumbnail ? {
|
||||
url: parsed.url,
|
||||
} : null,
|
||||
image: isImage && parsed.embed.image ? {
|
||||
url: parsed.url,
|
||||
} : null,
|
||||
}] : null,
|
||||
embeds: parsed.embed
|
||||
? [
|
||||
{
|
||||
title: parsed.embed.title ?? null,
|
||||
description: parsed.embed.description ?? null,
|
||||
url: parsed.url ?? null,
|
||||
timestamp: parsed.embed.timestamp ? image.created_at.toISOString() : null,
|
||||
color: parsed.embed.color ?? null,
|
||||
footer: parsed.embed.footer
|
||||
? {
|
||||
text: parsed.embed.footer,
|
||||
}
|
||||
: null,
|
||||
thumbnail:
|
||||
isImage && parsed.embed.thumbnail
|
||||
? {
|
||||
url: parsed.url,
|
||||
}
|
||||
: null,
|
||||
image:
|
||||
isImage && parsed.embed.image
|
||||
? {
|
||||
url: parsed.url,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
]
|
||||
: null,
|
||||
};
|
||||
|
||||
const res = await fetch(config.discord.url, {
|
||||
|
@ -88,7 +107,9 @@ export async function sendUpload(user: User, image: Image, host: string) {
|
|||
|
||||
if (!res.ok) {
|
||||
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}`);
|
||||
}
|
||||
|
||||
|
@ -104,16 +125,22 @@ export async function sendShorten(user: User, url: Url, host: string) {
|
|||
username: config.discord.username,
|
||||
avatar_url: config.discord.avatar_url,
|
||||
content: parsed.content ?? null,
|
||||
embeds: parsed.embed ? [{
|
||||
title: parsed.embed.title ?? null,
|
||||
description: parsed.embed.description ?? null,
|
||||
url: parsed.url ?? null,
|
||||
timestamp: parsed.embed.timestamp ? url.created_at.toISOString() : null,
|
||||
color: parsed.embed.color ?? null,
|
||||
footer: parsed.embed.footer ? {
|
||||
text: parsed.embed.footer,
|
||||
} : null,
|
||||
}] : null,
|
||||
embeds: parsed.embed
|
||||
? [
|
||||
{
|
||||
title: parsed.embed.title ?? null,
|
||||
description: parsed.embed.description ?? null,
|
||||
url: parsed.url ?? null,
|
||||
timestamp: parsed.embed.timestamp ? url.created_at.toISOString() : null,
|
||||
color: parsed.embed.color ?? null,
|
||||
footer: parsed.embed.footer
|
||||
? {
|
||||
text: parsed.embed.footer,
|
||||
}
|
||||
: null,
|
||||
},
|
||||
]
|
||||
: null,
|
||||
};
|
||||
|
||||
const res = await fetch(config.discord.url, {
|
||||
|
@ -125,8 +152,10 @@ export async function sendShorten(user: User, url: Url, host: string) {
|
|||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,45 +1,45 @@
|
|||
// https://github.com/toptal/haste-server/blob/master/static/application.js#L167-L174
|
||||
// Popular extension map
|
||||
const exts = {
|
||||
'md': 'Markdown',
|
||||
'css': 'CSS',
|
||||
'js': 'JavaScript',
|
||||
'json': 'JSON',
|
||||
'html': 'HTML',
|
||||
'ts': 'TypeScript',
|
||||
'java': 'Java',
|
||||
'py': 'Python',
|
||||
'rb': 'Ruby',
|
||||
'sh': 'Shell',
|
||||
'php': 'PHP',
|
||||
'pl': 'Perl',
|
||||
'sql': 'SQL',
|
||||
'xml': 'XML',
|
||||
'yml': 'YAML',
|
||||
'yaml': 'YAML',
|
||||
'c': 'C',
|
||||
'cpp': 'C++',
|
||||
'cs': 'C#',
|
||||
'go': 'Go',
|
||||
'h': 'C/C++ Header',
|
||||
'txt': 'Text',
|
||||
'dockerfile': 'Dockerfile',
|
||||
'toml': 'TOML',
|
||||
'ini': 'INI',
|
||||
'bat': 'Batch File',
|
||||
'tex': 'TeX',
|
||||
'r': 'R',
|
||||
'lua': 'Lua',
|
||||
'ps1': 'PowerShell',
|
||||
'rst': 'reStructuredText',
|
||||
'rs': 'Rust',
|
||||
'swift': 'Swift',
|
||||
'scss': 'SCSS',
|
||||
'less': 'LESS',
|
||||
'scala': 'Scala',
|
||||
'kotlin': 'Kotlin',
|
||||
'vb': 'Visual Basic',
|
||||
'vim': 'Vim Script',
|
||||
md: 'Markdown',
|
||||
css: 'CSS',
|
||||
js: 'JavaScript',
|
||||
json: 'JSON',
|
||||
html: 'HTML',
|
||||
ts: 'TypeScript',
|
||||
java: 'Java',
|
||||
py: 'Python',
|
||||
rb: 'Ruby',
|
||||
sh: 'Shell',
|
||||
php: 'PHP',
|
||||
pl: 'Perl',
|
||||
sql: 'SQL',
|
||||
xml: 'XML',
|
||||
yml: 'YAML',
|
||||
yaml: 'YAML',
|
||||
c: 'C',
|
||||
cpp: 'C++',
|
||||
cs: 'C#',
|
||||
go: 'Go',
|
||||
h: 'C/C++ Header',
|
||||
txt: 'Text',
|
||||
dockerfile: 'Dockerfile',
|
||||
toml: 'TOML',
|
||||
ini: 'INI',
|
||||
bat: 'Batch File',
|
||||
tex: 'TeX',
|
||||
r: 'R',
|
||||
lua: 'Lua',
|
||||
ps1: 'PowerShell',
|
||||
rst: 'reStructuredText',
|
||||
rs: 'Rust',
|
||||
swift: 'Swift',
|
||||
scss: 'SCSS',
|
||||
less: 'LESS',
|
||||
scala: 'Scala',
|
||||
kotlin: 'Kotlin',
|
||||
vb: 'Visual Basic',
|
||||
vim: 'Vim Script',
|
||||
};
|
||||
|
||||
export default exts;
|
||||
export default exts;
|
||||
|
|
|
@ -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 = {};
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,11 @@ export default function login() {
|
|||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
|
||||
|
||||
const res = await useFetch('/api/user');
|
||||
if (res.error) {
|
||||
if (res.error === 'oauth token expired') return router.push(res.redirect_uri);
|
||||
|
||||
|
||||
return router.push('/auth/login?url=' + router.route);
|
||||
}
|
||||
|
||||
|
@ -29,4 +29,4 @@ export default function login() {
|
|||
}, []);
|
||||
|
||||
return { loading };
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,7 @@ export default class Logger {
|
|||
public name: string;
|
||||
|
||||
static get(clas: any) {
|
||||
if (typeof clas !== 'function')
|
||||
if (typeof clas !== 'string') throw new Error('not string/function');
|
||||
if (typeof clas !== 'function') if (typeof clas !== 'string') throw new Error('not string/function');
|
||||
|
||||
const name = clas.name ?? clas;
|
||||
|
||||
|
@ -28,11 +27,7 @@ export default class Logger {
|
|||
|
||||
error(...args: any[]) {
|
||||
console.log(
|
||||
this.formatMessage(
|
||||
LoggerLevel.ERROR,
|
||||
this.name,
|
||||
args.map((error) => error.stack ?? error).join(' ')
|
||||
)
|
||||
this.formatMessage(LoggerLevel.ERROR, this.name, args.map((error) => error.stack ?? error).join(' '))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -49,4 +44,4 @@ export default class Logger {
|
|||
return red('error');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,21 +3,23 @@ import { discord_auth, github_auth } from 'lib/oauth';
|
|||
import { notNull } from 'lib/util';
|
||||
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
|
||||
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 oauth_providers = [];
|
||||
|
||||
if (ghEnabled) oauth_providers.push({
|
||||
name: 'GitHub',
|
||||
url: '/api/auth/oauth/github',
|
||||
});
|
||||
if (discEnabled) oauth_providers.push({
|
||||
name: 'Discord',
|
||||
url: '/api/auth/oauth/discord',
|
||||
});
|
||||
if (ghEnabled)
|
||||
oauth_providers.push({
|
||||
name: 'GitHub',
|
||||
url: '/api/auth/oauth/github',
|
||||
});
|
||||
if (discEnabled)
|
||||
oauth_providers.push({
|
||||
name: 'Discord',
|
||||
url: '/api/auth/oauth/discord',
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
@ -29,4 +31,4 @@ export const getServerSideProps: GetServerSideProps = async ctx => {
|
|||
oauth_providers: JSON.stringify(oauth_providers),
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -21,7 +21,7 @@ export type NextApiReq = NextApiRequest & {
|
|||
getCookie: (name: string) => string | null;
|
||||
cleanCookie: (name: string) => void;
|
||||
files?: NextApiFile[];
|
||||
}
|
||||
};
|
||||
|
||||
export type NextApiRes = NextApiResponse & {
|
||||
error: (message: string) => void;
|
||||
|
@ -30,95 +30,108 @@ export type NextApiRes = NextApiResponse & {
|
|||
json: (json: Record<string, any>, status?: number) => void;
|
||||
ratelimited: (remaining: number) => 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 = (
|
||||
res: NextApiResponse,
|
||||
name: string,
|
||||
value: unknown,
|
||||
options: CookieSerializeOptions = {}
|
||||
) => {
|
||||
|
||||
if ('maxAge' in options) {
|
||||
options.expires = new Date(Date.now() + options.maxAge * 1000);
|
||||
options.maxAge /= 1000;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { readFile } from 'fs/promises';
|
||||
|
||||
export type Mimes = [string, string[]][]
|
||||
export type Mimes = [string, string[]][];
|
||||
|
||||
export async function guess(extension: string): Promise<string> {
|
||||
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';
|
||||
|
||||
return mime[1][0];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
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) => {
|
||||
const res = await fetch('https://api.github.com/user', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${access_token}`,
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
},
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
|
@ -13,15 +14,18 @@ export const github_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) => {
|
||||
const res = await fetch('https://discord.com/api/users/@me', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${access_token}`,
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
},
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
|
||||
return res.json();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue