mirror of
https://github.com/diced/zipline.git
synced 2025-12-09 22:30:44 -08:00
Compare commits
3 Commits
v3.7.0
...
feature/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7df4a578b | ||
|
|
38e30b2525 | ||
|
|
535600edc8 |
@@ -1,10 +0,0 @@
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-18
|
||||
|
||||
RUN usermod -l zipline node \
|
||||
&& groupmod -n zipline node \
|
||||
&& usermod -d /home/zipline zipline \
|
||||
&& echo "zipline ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers.d/zipline \
|
||||
&& chmod 0440 /etc/sudoers.d/zipline \
|
||||
&& sudo apt-get update && apt-get install gnupg2 -y
|
||||
|
||||
USER zipline
|
||||
@@ -2,15 +2,12 @@
|
||||
"name": "Zipline Codespace",
|
||||
"dockerComposeFile": "docker-compose.yml",
|
||||
"service": "app",
|
||||
"workspaceFolder": "/zipline",
|
||||
"workspaceFolder": "/workspace",
|
||||
"forwardPorts": [3000, 5432],
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/common-utils:2": {
|
||||
"username": "zipline"
|
||||
},
|
||||
"ghcr.io/devcontainers/features/docker-in-docker:1": {
|
||||
"dockerDashComposeVersion": "v2",
|
||||
"installDockerBuildx": true
|
||||
}
|
||||
"ghcr.io/devcontainers/features/common-utils:2": {},
|
||||
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {},
|
||||
"ghcr.io/devcontainers/features/node:1": {}
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
@@ -23,34 +20,22 @@
|
||||
},
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"files.autoSave": "afterDelay",
|
||||
"terminal.integrated.persistentSessionReviveProcess": "never",
|
||||
"terminal.integrated.defaultProfile.linux": "zsh",
|
||||
"terminal.integrated.profiles.linux": {
|
||||
"zsh": {
|
||||
"path": "/bin/zsh",
|
||||
"env": {
|
||||
"ZSH_THEME": "devcontainers"
|
||||
}
|
||||
}
|
||||
}
|
||||
"files.autoSave": "afterDelay"
|
||||
},
|
||||
"extensions": ["prisma.prisma", "esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
|
||||
}
|
||||
},
|
||||
"remoteUser": "zipline",
|
||||
"updateRemoteUserUID": true,
|
||||
"remoteEnv": {
|
||||
"CORE_DATABASE_URL": "postgres://postgres:postgres@localhost/zip10"
|
||||
},
|
||||
"portsAttributes": {
|
||||
"3000": {
|
||||
"label": "Zipline",
|
||||
"3000": {
|
||||
"label": "Zipline",
|
||||
"onAutoForward": "openBrowser"
|
||||
},
|
||||
"5432": {
|
||||
"label": "Postgres"
|
||||
}
|
||||
},
|
||||
"postCreateCommand": "sudo chown -R zipline:zipline /zipline && yarn install"
|
||||
},
|
||||
"5432": {
|
||||
"label": "Postgres"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: Dockerfile
|
||||
image: mcr.microsoft.com/vscode/devcontainers/javascript-node:0-18
|
||||
volumes:
|
||||
- ../:/zipline:cached
|
||||
- uploads:/zipline/uploads
|
||||
- node_modules:/zipline/node_modules
|
||||
- ..:/workspace:cached
|
||||
network_mode: service:db
|
||||
command: sleep infinity
|
||||
user: zipline
|
||||
db:
|
||||
image: postgres:latest
|
||||
restart: unless-stopped
|
||||
@@ -21,5 +19,4 @@ services:
|
||||
|
||||
volumes:
|
||||
pg_data:
|
||||
uploads:
|
||||
node_modules:
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
.yarn
|
||||
.devcontainer
|
||||
.github
|
||||
.next
|
||||
.vscode
|
||||
@@ -1,13 +1,5 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": [
|
||||
"next",
|
||||
"next/core-web-vitals",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"plugins": ["unused-imports", "@typescript-eslint"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"extends": ["next", "next/core-web-vitals", "plugin:prettier/recommended"],
|
||||
"rules": {
|
||||
"linebreak-style": ["error", "unix"],
|
||||
"quotes": [
|
||||
@@ -36,14 +28,6 @@
|
||||
"react/style-prop-object": "warn",
|
||||
"@next/next/no-img-element": "off",
|
||||
"jsx-a11y/alt-text": "off",
|
||||
"react/display-name": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"error",
|
||||
{ "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" }
|
||||
],
|
||||
"@typescript-eslint/ban-ts-comment": "off"
|
||||
"react/display-name": "off"
|
||||
}
|
||||
}
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Feature Request
|
||||
url: https://github.com/diced/zipline/discussions/new?category=ideas&title=Your%20breif%20description%20here&labels=feature
|
||||
about: Ask for a new feature
|
||||
- name: Zipline Discord
|
||||
url: https://discord.gg/EAhCRfGxCF
|
||||
about: Ask for help with anything related to Zipline!
|
||||
|
||||
24
.github/workflows/milestone.yml
vendored
24
.github/workflows/milestone.yml
vendored
@@ -1,24 +0,0 @@
|
||||
name: 'Issue/PR Milestones'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened]
|
||||
issues:
|
||||
types: [opened, reopened]
|
||||
|
||||
jobs:
|
||||
set:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/github-script@v3
|
||||
with:
|
||||
github-token: ${{secrets.GITHUB_TOKEN}}
|
||||
script: |
|
||||
const milestone = 2
|
||||
github.issues.update({
|
||||
issue_number: context.issue.number,
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
milestone
|
||||
})
|
||||
17
README.md
17
README.md
@@ -25,7 +25,7 @@ A ShareX/file upload server that is easy to use, packed with features, and with
|
||||
- Password Protected Uploads
|
||||
- URL shortening
|
||||
- Text uploading
|
||||
- URL Formats (uuid, dates, random alphanumeric, original name, zws, gfycat -> [animals](https://assets.gfycat.com/animals) [adjectives](https://assets.gfycat.com/adjectives))
|
||||
- URL Formats (uuid, dates, random alphanumeric, original name, zws)
|
||||
- Discord embeds (OG metadata)
|
||||
- Gallery viewer, and multiple file format support
|
||||
- Code highlighting
|
||||
@@ -35,16 +35,7 @@ A ShareX/file upload server that is easy to use, packed with features, and with
|
||||
- User invites
|
||||
- File Chunking (for large files)
|
||||
- File deletion once it reaches a certain amount of views
|
||||
- Easy setup instructions on [docs](https://zipl.vercel.app/) (One command install `docker compose up -d`)
|
||||
|
||||
<details>
|
||||
<summary>View upstream documentation</summary>
|
||||
|
||||
The website below provides documentation for more up-to-date features with the upstream branch. The normal documentation is for the latest release and is not updated unless a new release is made.
|
||||
|
||||
[https://trunk.zipline.diced.tech/](https://trunk.zipline.diced.tech/)
|
||||
|
||||
</details>
|
||||
- Easy setup instructions on [docs](https://zipl.vercel.app/) (One command install `docker-compose up -d`)
|
||||
|
||||
<details>
|
||||
<summary><h2>Screenshots (click)</h2></summary>
|
||||
@@ -60,13 +51,13 @@ The website below provides documentation for more up-to-date features with the u
|
||||
|
||||
## Install & run with Docker
|
||||
|
||||
This section requires [Docker](https://docs.docker.com/get-docker/) and [docker compose](https://docs.docker.com/compose/install/).
|
||||
This section requires [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/).
|
||||
|
||||
```shell
|
||||
git clone https://github.com/diced/zipline
|
||||
cd zipline
|
||||
|
||||
docker compose up -d
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### After installing
|
||||
|
||||
72
package.json
72
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zipline",
|
||||
"version": "3.7.0",
|
||||
"version": "3.7.0-rc4",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "npm-run-all build:server dev:run",
|
||||
@@ -14,11 +14,11 @@
|
||||
"migrate:dev": "prisma migrate dev --create-only",
|
||||
"start": "node dist",
|
||||
"lint": "next lint",
|
||||
"compose:up": "docker compose up",
|
||||
"compose:down": "docker compose down",
|
||||
"compose:build-dev": "docker compose --file docker-compose.dev.yml up --build",
|
||||
"compose:up-dev": "docker compose --file docker-compose.dev.yml up",
|
||||
"compose:down-dev": "docker compose --file docker-compose.dev.yml down",
|
||||
"docker:up": "docker-compose up",
|
||||
"docker:down": "docker-compose down",
|
||||
"docker:build-dev": "docker-compose --file docker-compose.dev.yml up --build",
|
||||
"docker:up-dev": "docker-compose --file docker-compose.dev.yml up",
|
||||
"docker:down-dev": "docker-compose --file docker-compose.dev.yml down",
|
||||
"scripts:read-config": "node --enable-source-maps dist/scripts/read-config",
|
||||
"scripts:import-dir": "node --enable-source-maps dist/scripts/import-dir",
|
||||
"scripts:list-users": "node --enable-source-maps dist/scripts/list-users",
|
||||
@@ -29,70 +29,66 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/server": "^11.10.0",
|
||||
"@mantine/core": "^6.0.4",
|
||||
"@mantine/dropzone": "^6.0.4",
|
||||
"@mantine/form": "^6.0.4",
|
||||
"@mantine/hooks": "^6.0.4",
|
||||
"@mantine/modals": "^6.0.4",
|
||||
"@mantine/next": "^6.0.4",
|
||||
"@mantine/notifications": "^6.0.4",
|
||||
"@mantine/prism": "^6.0.4",
|
||||
"@mantine/spotlight": "^6.0.4",
|
||||
"@mantine/core": "^5.10.5",
|
||||
"@mantine/dropzone": "^5.10.5",
|
||||
"@mantine/form": "^5.10.5",
|
||||
"@mantine/hooks": "^5.10.5",
|
||||
"@mantine/modals": "^5.10.5",
|
||||
"@mantine/next": "^5.10.5",
|
||||
"@mantine/notifications": "^5.10.5",
|
||||
"@mantine/prism": "^5.10.5",
|
||||
"@prisma/client": "^4.10.1",
|
||||
"@prisma/internals": "^4.10.1",
|
||||
"@prisma/migrate": "^4.10.1",
|
||||
"@sapphire/shapeshift": "^3.8.1",
|
||||
"@tabler/icons-react": "^2.11.0",
|
||||
"@tanstack/react-query": "^4.28.0",
|
||||
"@tanstack/react-query": "^4.24.10",
|
||||
"argon2": "^0.30.3",
|
||||
"cookie": "^0.5.0",
|
||||
"dayjs": "^1.11.7",
|
||||
"dotenv": "^16.0.3",
|
||||
"dotenv-expand": "^10.0.0",
|
||||
"exiftool-vendored": "^21.2.0",
|
||||
"fastify": "^4.15.0",
|
||||
"fastify": "^4.13.0",
|
||||
"fastify-plugin": "^4.5.0",
|
||||
"fflate": "^0.7.4",
|
||||
"find-my-way": "^7.6.0",
|
||||
"find-my-way": "^7.5.0",
|
||||
"katex": "^0.16.4",
|
||||
"mantine-datatable": "^2.2.6",
|
||||
"minio": "^7.0.33",
|
||||
"mantine-datatable": "^1.8.6",
|
||||
"minio": "^7.0.32",
|
||||
"ms": "canary",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"next": "^13.2.4",
|
||||
"next": "^13.2.1",
|
||||
"otplib": "^12.0.1",
|
||||
"prisma": "^4.10.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^8.0.6",
|
||||
"recharts": "^2.5.0",
|
||||
"recoil": "^0.7.7",
|
||||
"react-feather": "^2.0.10",
|
||||
"react-markdown": "^8.0.5",
|
||||
"recharts": "^2.4.3",
|
||||
"recoil": "^0.7.6",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sharp": "^0.32.0"
|
||||
"sharp": "^0.31.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/katex": "^0.16.0",
|
||||
"@types/minio": "^7.0.17",
|
||||
"@types/minio": "^7.0.16",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^18.15.10",
|
||||
"@types/node": "^18.14.2",
|
||||
"@types/qrcode": "^1.5.0",
|
||||
"@types/react": "^18.0.29",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/sharp": "^0.31.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.56.0",
|
||||
"@typescript-eslint/parser": "^5.56.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.36.0",
|
||||
"eslint-config-next": "^13.2.4",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint": "^8.35.0",
|
||||
"eslint-config-next": "^13.2.1",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.8.7",
|
||||
"tsup": "^6.7.0",
|
||||
"typescript": "^5.0.2"
|
||||
"prettier": "^2.8.4",
|
||||
"tsup": "^6.6.3",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `format` on the `File` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "File" DROP COLUMN "format";
|
||||
|
||||
-- DropEnum
|
||||
DROP TYPE "FileNameFormat";
|
||||
@@ -40,6 +40,13 @@ model Folder {
|
||||
files File[]
|
||||
}
|
||||
|
||||
enum FileNameFormat {
|
||||
UUID
|
||||
DATE
|
||||
RANDOM
|
||||
NAME
|
||||
}
|
||||
|
||||
model File {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
@@ -54,6 +61,7 @@ model File {
|
||||
embed Boolean @default(false)
|
||||
password String?
|
||||
invisible InvisibleFile?
|
||||
format FileNameFormat @default(RANDOM)
|
||||
|
||||
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||||
userId Int?
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
1750
public/animals.txt
1750
public/animals.txt
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
||||
import { Anchor } from '@mantine/core';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function AnchorNext({ href, ...others }) {
|
||||
return <Anchor component={Link} href={href} {...others} />;
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import { createStyles, Textarea } from '@mantine/core';
|
||||
import { createStyles, MantineSize, Textarea } from '@mantine/core';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
const useStyles = createStyles(() => ({
|
||||
const useStyles = createStyles((theme, { size }: { size: MantineSize }) => ({
|
||||
input: {
|
||||
fontFamily: 'monospace',
|
||||
fontSize: theme.fn.size({ size, sizes: theme.fontSizes }) - 2,
|
||||
height: '80vh',
|
||||
},
|
||||
}));
|
||||
|
||||
export default function CodeInput({ ...props }) {
|
||||
const { classes } = useStyles(null, { name: 'CodeInput' });
|
||||
const { classes } = useStyles({ size: 'md' }, { name: 'CodeInput' });
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
|
||||
398
src/components/File.tsx
Normal file
398
src/components/File.tsx
Normal file
@@ -0,0 +1,398 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Card,
|
||||
Group,
|
||||
LoadingOverlay,
|
||||
Modal,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
|
||||
import { useFolders } from 'lib/queries/folders';
|
||||
import { bytesToHuman } from 'lib/utils/bytes';
|
||||
import { relativeTime } from 'lib/utils/client';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
CalendarIcon,
|
||||
ClockIcon,
|
||||
CopyIcon,
|
||||
CrossIcon,
|
||||
DeleteIcon,
|
||||
DownloadIcon,
|
||||
ExternalLinkIcon,
|
||||
EyeIcon,
|
||||
HardDriveIcon,
|
||||
FileIcon,
|
||||
FolderMinusIcon,
|
||||
FolderPlusIcon,
|
||||
HashIcon,
|
||||
ImageIcon,
|
||||
InfoIcon,
|
||||
StarIcon,
|
||||
} from './icons';
|
||||
import MutedText from './MutedText';
|
||||
import Type from './Type';
|
||||
|
||||
export function FileMeta({ Icon, title, subtitle, ...other }) {
|
||||
return other.tooltip ? (
|
||||
<Group>
|
||||
<Icon size={24} />
|
||||
<Tooltip label={other.tooltip}>
|
||||
<Stack spacing={1}>
|
||||
<Text>{title}</Text>
|
||||
<MutedText size='md'>{subtitle}</MutedText>
|
||||
</Stack>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
) : (
|
||||
<Group>
|
||||
<Icon size={24} />
|
||||
<Stack spacing={1}>
|
||||
<Text>{title}</Text>
|
||||
<MutedText size='md'>{subtitle}</MutedText>
|
||||
</Stack>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function File({
|
||||
image,
|
||||
disableMediaPreview,
|
||||
exifEnabled,
|
||||
refreshImages,
|
||||
reducedActions = false,
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [overrideRender, setOverrideRender] = useState(false);
|
||||
const deleteFile = useFileDelete();
|
||||
const favoriteFile = useFileFavorite();
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const folders = useFolders();
|
||||
|
||||
const loading = deleteFile.isLoading || favoriteFile.isLoading;
|
||||
|
||||
const handleDelete = async () => {
|
||||
deleteFile.mutate(image.id, {
|
||||
onSuccess: () => {
|
||||
showNotification({
|
||||
title: 'File Deleted',
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <DeleteIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
onError: (res: any) => {
|
||||
showNotification({
|
||||
title: 'Failed to delete file',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
onSettled: () => {
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${image.url}`);
|
||||
setOpen(false);
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: '',
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
const handleFavorite = async () => {
|
||||
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 />,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const inFolder = image.folderId;
|
||||
|
||||
const refresh = () => {
|
||||
refreshImages();
|
||||
folders.refetch();
|
||||
};
|
||||
|
||||
const removeFromFolder = async () => {
|
||||
const res = await useFetch('/api/user/folders/' + image.folderId, 'DELETE', {
|
||||
file: Number(image.id),
|
||||
});
|
||||
|
||||
refresh();
|
||||
|
||||
if (!res.error) {
|
||||
showNotification({
|
||||
title: 'Removed from folder',
|
||||
message: res.name,
|
||||
color: 'green',
|
||||
icon: <FolderMinusIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Failed to remove from folder',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const addToFolder = async (t) => {
|
||||
const res = await useFetch('/api/user/folders/' + t, 'POST', {
|
||||
file: Number(image.id),
|
||||
});
|
||||
|
||||
refresh();
|
||||
|
||||
if (!res.error) {
|
||||
showNotification({
|
||||
title: 'Added to folder',
|
||||
message: res.name,
|
||||
color: 'green',
|
||||
icon: <FolderPlusIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Failed to add to folder',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createFolder = (t) => {
|
||||
useFetch('/api/user/folders', 'POST', {
|
||||
name: t,
|
||||
add: [Number(image.id)],
|
||||
}).then((res) => {
|
||||
refresh();
|
||||
|
||||
if (!res.error) {
|
||||
showNotification({
|
||||
title: 'Created & added to folder',
|
||||
message: res.name,
|
||||
color: 'green',
|
||||
icon: <FolderPlusIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Failed to create folder',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
});
|
||||
return { value: t, label: t };
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>{image.name}</Title>} size='xl'>
|
||||
<LoadingOverlay visible={loading} />
|
||||
<Stack>
|
||||
<Type
|
||||
file={image}
|
||||
src={`/r/${encodeURI(image.name)}`}
|
||||
alt={image.name}
|
||||
popup
|
||||
sx={{ minHeight: 200 }}
|
||||
style={{ minHeight: 200 }}
|
||||
disableMediaPreview={false}
|
||||
overrideRender={overrideRender}
|
||||
setOverrideRender={setOverrideRender}
|
||||
/>
|
||||
<SimpleGrid
|
||||
my='md'
|
||||
cols={3}
|
||||
breakpoints={[
|
||||
{ maxWidth: 600, cols: 1 },
|
||||
{ maxWidth: 900, cols: 2 },
|
||||
{ maxWidth: 1200, cols: 3 },
|
||||
]}
|
||||
>
|
||||
<FileMeta Icon={FileIcon} title='Name' subtitle={image.name} />
|
||||
<FileMeta Icon={ImageIcon} title='Type' subtitle={image.mimetype} />
|
||||
<FileMeta Icon={HardDriveIcon} title='Size' subtitle={bytesToHuman(image.size || 0)} />
|
||||
<FileMeta Icon={EyeIcon} title='Views' subtitle={image?.views?.toLocaleString()} />
|
||||
{image.maxViews && (
|
||||
<FileMeta
|
||||
Icon={EyeIcon}
|
||||
title='Max views'
|
||||
subtitle={image?.maxViews?.toLocaleString()}
|
||||
tooltip={`This file will be deleted after being viewed ${image?.maxViews?.toLocaleString()} times.`}
|
||||
/>
|
||||
)}
|
||||
<FileMeta
|
||||
Icon={CalendarIcon}
|
||||
title='Uploaded'
|
||||
subtitle={relativeTime(new Date(image.createdAt))}
|
||||
tooltip={new Date(image?.createdAt).toLocaleString()}
|
||||
/>
|
||||
{image.expiresAt && !reducedActions && (
|
||||
<FileMeta
|
||||
Icon={ClockIcon}
|
||||
title='Expires'
|
||||
subtitle={relativeTime(new Date(image.expiresAt))}
|
||||
tooltip={new Date(image.expiresAt).toLocaleString()}
|
||||
/>
|
||||
)}
|
||||
<FileMeta Icon={HashIcon} title='ID' subtitle={image.id} />
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
<Group position='apart' my='md'>
|
||||
<Group position='left'>
|
||||
{exifEnabled && !reducedActions && (
|
||||
<Tooltip label='View Metadata'>
|
||||
<ActionIcon
|
||||
color='blue'
|
||||
variant='filled'
|
||||
onClick={() => window.open(`/dashboard/metadata/${image.id}`, '_blank')}
|
||||
>
|
||||
<InfoIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{reducedActions ? null : inFolder && !folders.isLoading ? (
|
||||
<Tooltip
|
||||
label={`Remove from folder "${
|
||||
folders.data.find((f) => f.id === image.folderId)?.name ?? ''
|
||||
}"`}
|
||||
>
|
||||
<ActionIcon
|
||||
color='red'
|
||||
variant='filled'
|
||||
onClick={removeFromFolder}
|
||||
loading={folders.isLoading}
|
||||
>
|
||||
<FolderMinusIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip label='Add to folder'>
|
||||
<Select
|
||||
onChange={addToFolder}
|
||||
placeholder='Add to folder'
|
||||
data={[
|
||||
...(folders.data ? folders.data : []).map((folder) => ({
|
||||
value: String(folder.id),
|
||||
label: `${folder.id}: ${folder.name}`,
|
||||
})),
|
||||
]}
|
||||
searchable
|
||||
creatable
|
||||
getCreateLabel={(query) => `Create folder "${query}"`}
|
||||
onCreate={createFolder}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Group>
|
||||
<Group position='right'>
|
||||
{reducedActions ? null : (
|
||||
<>
|
||||
<Tooltip label='Delete file'>
|
||||
<ActionIcon color='red' variant='filled' onClick={handleDelete}>
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label={image.favorite ? 'Unfavorite' : 'Favorite'}>
|
||||
<ActionIcon
|
||||
color={image.favorite ? 'yellow' : 'gray'}
|
||||
variant='filled'
|
||||
onClick={handleFavorite}
|
||||
>
|
||||
<StarIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Tooltip label='Open in new tab'>
|
||||
<ActionIcon color='blue' variant='filled' onClick={() => window.open(image.url, '_blank')}>
|
||||
<ExternalLinkIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label='Copy URL'>
|
||||
<ActionIcon color='blue' variant='filled' onClick={handleCopy}>
|
||||
<CopyIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label='Download'>
|
||||
<ActionIcon
|
||||
color='blue'
|
||||
variant='filled'
|
||||
onClick={() => window.open(`/r/${encodeURI(image.name)}?download=true`, '_blank')}
|
||||
>
|
||||
<DownloadIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Group>
|
||||
</Modal>
|
||||
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md'>
|
||||
<Card.Section>
|
||||
<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',
|
||||
}}
|
||||
src={`/r/${encodeURI(image.name)}`}
|
||||
alt={image.name}
|
||||
onClick={() => setOpen(true)}
|
||||
disableMediaPreview={disableMediaPreview}
|
||||
/>
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -9,35 +9,31 @@ import {
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useClipboard, useMediaQuery } from '@mantine/hooks';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconAlarm,
|
||||
IconCalendarPlus,
|
||||
IconClipboardCopy,
|
||||
IconDeviceSdCard,
|
||||
IconExternalLink,
|
||||
IconEye,
|
||||
IconEyeglass,
|
||||
IconFile,
|
||||
IconFileDownload,
|
||||
IconFolderCancel,
|
||||
IconFolderMinus,
|
||||
IconFolderPlus,
|
||||
IconHash,
|
||||
IconInfoCircle,
|
||||
IconPhoto,
|
||||
IconPhotoCancel,
|
||||
IconPhotoMinus,
|
||||
IconPhotoStar,
|
||||
} from '@tabler/icons-react';
|
||||
import useFetch, { ApiError } from 'hooks/useFetch';
|
||||
import { useFileDelete, useFileFavorite, UserFilesResponse } from 'lib/queries/files';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useFileDelete, useFileFavorite } from 'lib/queries/files';
|
||||
import { useFolders } from 'lib/queries/folders';
|
||||
import { bytesToHuman } from 'lib/utils/bytes';
|
||||
import { relativeTime } from 'lib/utils/client';
|
||||
import { useState } from 'react';
|
||||
import { FileMeta } from '.';
|
||||
import {
|
||||
CalendarIcon,
|
||||
ClockIcon,
|
||||
CopyIcon,
|
||||
CrossIcon,
|
||||
DeleteIcon,
|
||||
DownloadIcon,
|
||||
ExternalLinkIcon,
|
||||
EyeIcon,
|
||||
FileIcon,
|
||||
FolderMinusIcon,
|
||||
FolderPlusIcon,
|
||||
HashIcon,
|
||||
ImageIcon,
|
||||
InfoIcon,
|
||||
StarIcon,
|
||||
} from '../icons';
|
||||
import Type from '../Type';
|
||||
|
||||
export default function FileModal({
|
||||
@@ -48,16 +44,14 @@ export default function FileModal({
|
||||
refresh,
|
||||
reducedActions = false,
|
||||
exifEnabled,
|
||||
compress,
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
file: UserFilesResponse;
|
||||
file: any;
|
||||
loading: boolean;
|
||||
refresh: () => void;
|
||||
reducedActions?: boolean;
|
||||
exifEnabled?: boolean;
|
||||
compress: boolean;
|
||||
}) {
|
||||
const deleteFile = useFileDelete();
|
||||
const favoriteFile = useFileFavorite();
|
||||
@@ -73,16 +67,16 @@ export default function FileModal({
|
||||
title: 'File Deleted',
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <IconPhotoMinus size='1rem' />,
|
||||
icon: <DeleteIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
onError: (res: ApiError) => {
|
||||
onError: (res: any) => {
|
||||
showNotification({
|
||||
title: 'Failed to delete file',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <IconPhotoCancel size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -105,7 +99,7 @@ export default function FileModal({
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: '',
|
||||
icon: <IconClipboardCopy size='1rem' />,
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -117,16 +111,16 @@ export default function FileModal({
|
||||
showNotification({
|
||||
title: 'The file is now ' + (!file.favorite ? 'favorited' : 'unfavorited'),
|
||||
message: '',
|
||||
icon: <IconPhotoStar size='1rem' />,
|
||||
icon: <StarIcon />,
|
||||
});
|
||||
},
|
||||
|
||||
onError: (res: { error: string }) => {
|
||||
onError: (res: any) => {
|
||||
showNotification({
|
||||
title: 'Failed to favorite file',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <IconPhotoCancel size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
},
|
||||
}
|
||||
@@ -147,14 +141,14 @@ export default function FileModal({
|
||||
title: 'Removed from folder',
|
||||
message: res.name,
|
||||
color: 'green',
|
||||
icon: <IconFolderMinus size='1rem' />,
|
||||
icon: <FolderMinusIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Failed to remove from folder',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <IconFolderCancel size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -171,14 +165,14 @@ export default function FileModal({
|
||||
title: 'Added to folder',
|
||||
message: res.name,
|
||||
color: 'green',
|
||||
icon: <IconFolderPlus size='1rem' />,
|
||||
icon: <FolderPlusIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Failed to add to folder',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <IconFolderCancel size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -195,14 +189,14 @@ export default function FileModal({
|
||||
title: 'Created & added to folder',
|
||||
message: res.name,
|
||||
color: 'green',
|
||||
icon: <IconFolderPlus size='1rem' />,
|
||||
icon: <FolderPlusIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Failed to create folder',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <IconFolderCancel size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -210,18 +204,12 @@ export default function FileModal({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={open}
|
||||
onClose={() => setOpen(false)}
|
||||
title={<Title>{file.name}</Title>}
|
||||
size='auto'
|
||||
fullScreen={useMediaQuery('(max-width: 600px)')}
|
||||
>
|
||||
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>{file.name}</Title>} size='xl'>
|
||||
<LoadingOverlay visible={loading} />
|
||||
<Stack>
|
||||
<Type
|
||||
file={file}
|
||||
src={`/r/${encodeURI(file.name)}?compress=${compress}`}
|
||||
src={`/r/${encodeURI(file.name)}`}
|
||||
alt={file.name}
|
||||
popup
|
||||
sx={{ minHeight: 200 }}
|
||||
@@ -239,33 +227,32 @@ export default function FileModal({
|
||||
{ maxWidth: 1200, cols: 3 },
|
||||
]}
|
||||
>
|
||||
<FileMeta Icon={IconFile} title='Name' subtitle={file.name} />
|
||||
<FileMeta Icon={IconPhoto} title='Type' subtitle={file.mimetype} />
|
||||
<FileMeta Icon={IconDeviceSdCard} title='Size' subtitle={bytesToHuman(file.size || 0)} />
|
||||
<FileMeta Icon={IconEye} title='Views' subtitle={file?.views?.toLocaleString()} />
|
||||
<FileMeta Icon={FileIcon} title='Name' subtitle={file.name} />
|
||||
<FileMeta Icon={ImageIcon} title='Type' subtitle={file.mimetype} />
|
||||
<FileMeta Icon={EyeIcon} title='Views' subtitle={file?.views?.toLocaleString()} />
|
||||
{file.maxViews && (
|
||||
<FileMeta
|
||||
Icon={IconEyeglass}
|
||||
Icon={EyeIcon}
|
||||
title='Max views'
|
||||
subtitle={file?.maxViews?.toLocaleString()}
|
||||
tooltip={`This file will be deleted after being viewed ${file?.maxViews?.toLocaleString()} times.`}
|
||||
/>
|
||||
)}
|
||||
<FileMeta
|
||||
Icon={IconCalendarPlus}
|
||||
Icon={CalendarIcon}
|
||||
title='Uploaded'
|
||||
subtitle={relativeTime(new Date(file.createdAt))}
|
||||
tooltip={new Date(file?.createdAt).toLocaleString()}
|
||||
/>
|
||||
{file.expiresAt && !reducedActions && (
|
||||
<FileMeta
|
||||
Icon={IconAlarm}
|
||||
Icon={ClockIcon}
|
||||
title='Expires'
|
||||
subtitle={relativeTime(new Date(file.expiresAt))}
|
||||
tooltip={new Date(file.expiresAt).toLocaleString()}
|
||||
/>
|
||||
)}
|
||||
<FileMeta Icon={IconHash} title='ID' subtitle={file.id} />
|
||||
<FileMeta Icon={HashIcon} title='ID' subtitle={file.id} />
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
|
||||
@@ -278,7 +265,7 @@ export default function FileModal({
|
||||
variant='filled'
|
||||
onClick={() => window.open(`/dashboard/metadata/${file.id}`, '_blank')}
|
||||
>
|
||||
<IconInfoCircle size='1rem' />
|
||||
<InfoIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -287,7 +274,7 @@ export default function FileModal({
|
||||
label={`Remove from folder "${folders.data.find((f) => f.id === file.folderId)?.name ?? ''}"`}
|
||||
>
|
||||
<ActionIcon color='red' variant='filled' onClick={removeFromFolder} loading={folders.isLoading}>
|
||||
<IconFolderMinus size='1rem' />
|
||||
<FolderMinusIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
) : (
|
||||
@@ -314,7 +301,7 @@ export default function FileModal({
|
||||
<>
|
||||
<Tooltip label='Delete file'>
|
||||
<ActionIcon color='red' variant='filled' onClick={handleDelete}>
|
||||
<IconPhotoMinus size='1rem' />
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
@@ -324,7 +311,7 @@ export default function FileModal({
|
||||
variant='filled'
|
||||
onClick={handleFavorite}
|
||||
>
|
||||
<IconPhotoStar size='1rem' />
|
||||
<StarIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</>
|
||||
@@ -332,13 +319,13 @@ export default function FileModal({
|
||||
|
||||
<Tooltip label='Open in new tab'>
|
||||
<ActionIcon color='blue' variant='filled' onClick={() => window.open(file.url, '_blank')}>
|
||||
<IconExternalLink size='1rem' />
|
||||
<ExternalLinkIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label='Copy URL'>
|
||||
<ActionIcon color='blue' variant='filled' onClick={handleCopy}>
|
||||
<IconClipboardCopy size='1rem' />
|
||||
<CopyIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
@@ -348,7 +335,7 @@ export default function FileModal({
|
||||
variant='filled'
|
||||
onClick={() => window.open(`/r/${encodeURI(file.name)}?download=true`, '_blank')}
|
||||
>
|
||||
<IconFileDownload size='1rem' />
|
||||
<DownloadIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
@@ -34,7 +34,6 @@ export default function File({
|
||||
exifEnabled,
|
||||
refreshImages,
|
||||
reducedActions = false,
|
||||
onDash,
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const deleteFile = useFileDelete();
|
||||
@@ -58,10 +57,9 @@ export default function File({
|
||||
refresh={refresh}
|
||||
reducedActions={reducedActions}
|
||||
exifEnabled={exifEnabled}
|
||||
compress={onDash}
|
||||
/>
|
||||
|
||||
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md' onClick={() => setOpen(true)}>
|
||||
<Card sx={{ maxWidth: '100%', height: '100%' }} shadow='md'>
|
||||
<Card.Section>
|
||||
<LoadingOverlay visible={loading} />
|
||||
<Type
|
||||
@@ -80,8 +78,9 @@ export default function File({
|
||||
width: '100%',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
src={`/r/${encodeURI(image.name)}?compress=${onDash}`}
|
||||
src={`/r/${encodeURI(image.name)}`}
|
||||
alt={image.name}
|
||||
onClick={() => setOpen(true)}
|
||||
disableMediaPreview={disableMediaPreview}
|
||||
/>
|
||||
</Card.Section>
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
Navbar,
|
||||
NavLink,
|
||||
Paper,
|
||||
rem,
|
||||
ScrollArea,
|
||||
Select,
|
||||
Text,
|
||||
@@ -24,39 +23,36 @@ import {
|
||||
import { useClipboard, useMediaQuery } from '@mantine/hooks';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconBackspace,
|
||||
IconBrandDiscordFilled,
|
||||
IconBrandGithubFilled,
|
||||
IconBrandGoogle,
|
||||
IconBrush,
|
||||
IconClipboardCopy,
|
||||
IconExternalLink,
|
||||
IconFiles,
|
||||
IconFileText,
|
||||
IconFileUpload,
|
||||
IconFolders,
|
||||
IconGraph,
|
||||
IconHome,
|
||||
IconLink,
|
||||
IconLogout,
|
||||
IconReload,
|
||||
IconSettings,
|
||||
IconTag,
|
||||
IconUpload,
|
||||
IconUser,
|
||||
IconUserCog,
|
||||
IconUsers,
|
||||
} from '@tabler/icons-react';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useVersion } from 'lib/queries/version';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
import { capitalize } from 'lib/utils/client';
|
||||
import { UserExtended } from 'middleware/withZipline';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import {
|
||||
ActivityIcon,
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
CrossIcon,
|
||||
DeleteIcon,
|
||||
DiscordIcon,
|
||||
ExternalLinkIcon,
|
||||
FileIcon,
|
||||
FolderIcon,
|
||||
GitHubIcon,
|
||||
GoogleIcon,
|
||||
HomeIcon,
|
||||
LinkIcon,
|
||||
LogoutIcon,
|
||||
PencilIcon,
|
||||
SettingsIcon,
|
||||
TagIcon,
|
||||
TypeIcon,
|
||||
UploadIcon,
|
||||
UserIcon,
|
||||
} from './icons';
|
||||
import { friendlyThemeName, themes } from './Theming';
|
||||
|
||||
export type NavbarItems = {
|
||||
@@ -64,67 +60,67 @@ export type NavbarItems = {
|
||||
text: string;
|
||||
link?: string;
|
||||
children?: NavbarItems[];
|
||||
if?: (user: UserExtended, props: unknown) => boolean;
|
||||
if?: (user: any, props: any) => boolean;
|
||||
};
|
||||
|
||||
const items: NavbarItems[] = [
|
||||
{
|
||||
icon: <IconHome size={18} />,
|
||||
icon: <HomeIcon size={18} />,
|
||||
text: 'Home',
|
||||
link: '/dashboard',
|
||||
},
|
||||
{
|
||||
icon: <IconFiles size={18} />,
|
||||
icon: <FileIcon size={18} />,
|
||||
text: 'Files',
|
||||
link: '/dashboard/files',
|
||||
},
|
||||
{
|
||||
icon: <IconFolders size={18} />,
|
||||
icon: <FolderIcon size={18} />,
|
||||
text: 'Folders',
|
||||
link: '/dashboard/folders',
|
||||
},
|
||||
{
|
||||
icon: <IconGraph size={18} />,
|
||||
icon: <ActivityIcon size={18} />,
|
||||
text: 'Stats',
|
||||
link: '/dashboard/stats',
|
||||
},
|
||||
{
|
||||
icon: <IconLink size={18} />,
|
||||
icon: <LinkIcon size={18} />,
|
||||
text: 'URLs',
|
||||
link: '/dashboard/urls',
|
||||
},
|
||||
{
|
||||
icon: <IconUpload size={18} />,
|
||||
icon: <UploadIcon size={18} />,
|
||||
text: 'Upload',
|
||||
children: [
|
||||
{
|
||||
icon: <IconFileUpload size={18} />,
|
||||
icon: <UploadIcon size={18} />,
|
||||
text: 'File',
|
||||
link: '/dashboard/upload/file',
|
||||
},
|
||||
{
|
||||
icon: <IconFileText size={18} />,
|
||||
icon: <TypeIcon size={18} />,
|
||||
text: 'Text',
|
||||
link: '/dashboard/upload/text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
icon: <IconUser size={18} />,
|
||||
icon: <UserIcon size={18} />,
|
||||
text: 'Administration',
|
||||
if: (user, _) => user.administrator as boolean,
|
||||
children: [
|
||||
{
|
||||
icon: <IconUsers size={18} />,
|
||||
icon: <UserIcon size={18} />,
|
||||
text: 'Users',
|
||||
link: '/dashboard/users',
|
||||
if: () => true,
|
||||
},
|
||||
{
|
||||
icon: <IconTag size={18} />,
|
||||
icon: <TagIcon size={18} />,
|
||||
text: 'Invites',
|
||||
link: '/dashboard/invites',
|
||||
if: (_, props: { invites: boolean }) => props.invites,
|
||||
if: (_, props) => props.invites,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -136,9 +132,9 @@ export default function Layout({ children, props }) {
|
||||
const { title, oauth_providers: unparsed } = props;
|
||||
const oauth_providers = JSON.parse(unparsed);
|
||||
const icons = {
|
||||
GitHub: IconBrandGithubFilled,
|
||||
Discord: IconBrandDiscordFilled,
|
||||
Google: IconBrandGoogle,
|
||||
GitHub: GitHubIcon,
|
||||
Discord: DiscordIcon,
|
||||
Google: GoogleIcon,
|
||||
};
|
||||
|
||||
for (const provider of oauth_providers) {
|
||||
@@ -171,7 +167,7 @@ export default function Layout({ children, props }) {
|
||||
title: `Theme changed to ${friendlyThemeName[value]}`,
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <IconBrush size='1rem' />,
|
||||
icon: <PencilIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -192,7 +188,7 @@ export default function Layout({ children, props }) {
|
||||
title: 'Token Reset Failed',
|
||||
message: a.error,
|
||||
color: 'red',
|
||||
icon: <IconReload size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
@@ -200,7 +196,7 @@ export default function Layout({ children, props }) {
|
||||
message:
|
||||
'Your token has been reset. You will need to update any uploaders to use this new token.',
|
||||
color: 'green',
|
||||
icon: <IconReload size='1rem' />,
|
||||
icon: <CheckIcon />,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -241,7 +237,7 @@ export default function Layout({ children, props }) {
|
||||
title: 'Token Copied',
|
||||
message: 'Your token has been copied to your clipboard.',
|
||||
color: 'green',
|
||||
icon: <IconClipboardCopy size='1rem' />,
|
||||
icon: <CheckIcon />,
|
||||
});
|
||||
|
||||
modals.closeAll();
|
||||
@@ -268,42 +264,42 @@ export default function Layout({ children, props }) {
|
||||
{children
|
||||
.filter((x) => (x.if ? x.if(user, props) : true))
|
||||
.map(({ icon, text, link }) => (
|
||||
<NavLink
|
||||
key={text}
|
||||
label={text}
|
||||
icon={icon}
|
||||
active={router.pathname === link}
|
||||
variant='light'
|
||||
component={Link}
|
||||
href={link}
|
||||
/>
|
||||
<Link href={link} key={text} passHref legacyBehavior>
|
||||
<NavLink
|
||||
component='a'
|
||||
label={text}
|
||||
icon={icon}
|
||||
active={router.pathname === link}
|
||||
variant='light'
|
||||
/>
|
||||
</Link>
|
||||
))}
|
||||
</NavLink>
|
||||
) : (
|
||||
<NavLink
|
||||
key={text}
|
||||
label={text}
|
||||
icon={icon}
|
||||
active={router.pathname === link}
|
||||
variant='light'
|
||||
component={Link}
|
||||
href={link}
|
||||
/>
|
||||
<Link href={link} key={text} passHref legacyBehavior>
|
||||
<NavLink
|
||||
component='a'
|
||||
label={text}
|
||||
icon={icon}
|
||||
active={router.pathname === link}
|
||||
variant='light'
|
||||
/>
|
||||
</Link>
|
||||
)
|
||||
)}
|
||||
</Navbar.Section>
|
||||
<Navbar.Section>
|
||||
{external_links.length
|
||||
? external_links.map(({ label, link }, i: number) => (
|
||||
<NavLink
|
||||
key={i}
|
||||
label={label}
|
||||
target='_blank'
|
||||
variant='light'
|
||||
icon={<IconExternalLink size={18} />}
|
||||
component={Link}
|
||||
href={link}
|
||||
/>
|
||||
? external_links.map(({ label, link }, i) => (
|
||||
<Link href={link} passHref key={i} legacyBehavior>
|
||||
<NavLink
|
||||
label={label}
|
||||
component='a'
|
||||
target='_blank'
|
||||
variant='light'
|
||||
icon={<ExternalLinkIcon />}
|
||||
/>
|
||||
</Link>
|
||||
))
|
||||
: null}
|
||||
</Navbar.Section>
|
||||
@@ -357,12 +353,14 @@ export default function Layout({ children, props }) {
|
||||
>
|
||||
<Menu.Target>
|
||||
<Button
|
||||
leftIcon={
|
||||
avatar ? <Image src={avatar} height={32} radius='md' /> : <IconUserCog size='1rem' />
|
||||
}
|
||||
variant='subtle'
|
||||
color='gray'
|
||||
compact
|
||||
leftIcon={avatar ? <Image src={avatar} height={32} radius='md' /> : <SettingsIcon />}
|
||||
sx={(t) => ({
|
||||
backgroundColor: 'inherit',
|
||||
'&:hover': {
|
||||
backgroundColor: t.other.hover,
|
||||
},
|
||||
color: t.colorScheme === 'dark' ? 'white' : 'black',
|
||||
})}
|
||||
size='xl'
|
||||
p='sm'
|
||||
>
|
||||
@@ -374,39 +372,19 @@ export default function Layout({ children, props }) {
|
||||
{user.username} ({user.id}){' '}
|
||||
{user.administrator && user.username !== 'administrator' ? '(Administrator)' : ''}
|
||||
</Menu.Label>
|
||||
<Menu.Item component={Link} icon={<IconFiles size='1rem' />} href='/dashboard/files'>
|
||||
Files
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
icon={<IconFileUpload size='1rem' />}
|
||||
href='/dashboard/upload/file'
|
||||
>
|
||||
Upload File
|
||||
</Menu.Item>
|
||||
<Menu.Item component={Link} icon={<IconLink size='1rem' />} href='/dashboard/urls'>
|
||||
Shorten URL
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Label>Settings</Menu.Label>
|
||||
<Menu.Item component={Link} icon={<IconSettings size='1rem' />} href='/dashboard/manage'>
|
||||
<Menu.Item component={Link} icon={<SettingsIcon />} href='/dashboard/manage'>
|
||||
Manage Account
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
icon={<IconClipboardCopy size='1rem' />}
|
||||
icon={<CopyIcon />}
|
||||
onClick={() => {
|
||||
openCopyToken();
|
||||
}}
|
||||
>
|
||||
Copy Token
|
||||
</Menu.Item>
|
||||
<Menu.Item icon={<IconLogout size='1rem' />} component={Link} href='/auth/logout'>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Label>Danger</Menu.Label>
|
||||
<Menu.Item
|
||||
icon={<IconBackspace size='1rem' />}
|
||||
icon={<DeleteIcon />}
|
||||
onClick={() => {
|
||||
openResetToken();
|
||||
}}
|
||||
@@ -414,13 +392,11 @@ export default function Layout({ children, props }) {
|
||||
>
|
||||
Reset Token
|
||||
</Menu.Item>
|
||||
<Menu.Item component={Link} icon={<LogoutIcon />} href='/auth/logout' color='red'>
|
||||
Logout
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
<>
|
||||
{oauth_providers.filter((x) =>
|
||||
user.oauth?.map(({ provider }) => provider.toLowerCase()).includes(x.name.toLowerCase())
|
||||
).length ? (
|
||||
<Menu.Label>Connected Accounts</Menu.Label>
|
||||
) : null}
|
||||
{oauth_providers
|
||||
.filter((x) =>
|
||||
user.oauth
|
||||
@@ -444,7 +420,7 @@ export default function Layout({ children, props }) {
|
||||
<Menu.Divider />
|
||||
) : null}
|
||||
</>
|
||||
<Menu.Item closeMenuOnClick={false} icon={<IconBrush size='1rem' />}>
|
||||
<Menu.Item closeMenuOnClick={false} icon={<PencilIcon />}>
|
||||
<Select
|
||||
size={useMediaQuery('(max-width: 768px)') ? 'md' : 'xs'}
|
||||
data={Object.keys(themes).map((t) => ({
|
||||
@@ -465,15 +441,9 @@ export default function Layout({ children, props }) {
|
||||
<Paper
|
||||
withBorder
|
||||
p='md'
|
||||
mr='md'
|
||||
mb='md'
|
||||
shadow='xs'
|
||||
sx={(theme) => ({
|
||||
'&[data-with-border]': {
|
||||
border: `${rem(1)} solid ${
|
||||
theme.colorScheme === 'dark' ? theme.colors.dark[5] : theme.colors.gray[0]
|
||||
}`,
|
||||
},
|
||||
sx={(t) => ({
|
||||
borderColor: t.colorScheme === 'dark' ? t.colors.dark[5] : t.colors.dark[0],
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
|
||||
5
src/components/Link.tsx
Normal file
5
src/components/Link.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { NextLink } from '@mantine/next';
|
||||
|
||||
export default function Link(props) {
|
||||
return <NextLink legacyBehavior {...props} />;
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
// https://mantine.dev/core/password-input/
|
||||
|
||||
import { Box, PasswordInput, Popover, Progress, Text } from '@mantine/core';
|
||||
import { IconCheck, IconCross } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
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'>
|
||||
{meets ? <IconCheck size='1rem' /> : <IconCross size='1rem' />} <Box ml='md'>{label}</Box>
|
||||
{meets ? <CheckIcon /> : <CrossIcon />} <Box ml='md'>{label}</Box>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Card, createStyles, Group, Text } from '@mantine/core';
|
||||
import { IconArrowDownRight, IconArrowUpRight } from '@tabler/icons-react';
|
||||
import { ArrowDownRight, ArrowUpRight } from 'react-feather';
|
||||
|
||||
const useStyles = createStyles((theme) => ({
|
||||
root: {
|
||||
padding: `calc(${theme.spacing.xl} * 1.5)`,
|
||||
padding: theme.spacing.xl * 1.5,
|
||||
},
|
||||
|
||||
value: {
|
||||
@@ -57,7 +57,7 @@ export default function StatCard({ stat }: StatsGridProps) {
|
||||
<>
|
||||
<Text color={stat.diff >= 0 ? 'teal' : 'red'} size='sm' weight={500} className={classes.diff}>
|
||||
<span>{stat.diff === Infinity ? '∞' : stat.diff}%</span>
|
||||
{stat.diff >= 0 ? <IconArrowUpRight size={16} /> : <IconArrowDownRight size={16} />}
|
||||
{stat.diff >= 0 ? <ArrowUpRight size={16} /> : <ArrowDownRight size={16} />}
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -15,15 +15,10 @@ import qogir_dark from 'lib/themes/qogir_dark';
|
||||
import { createEmotionCache, MantineProvider, MantineThemeOverride } from '@mantine/core';
|
||||
import { useColorScheme } from '@mantine/hooks';
|
||||
import { ModalsProvider } from '@mantine/modals';
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
import { SpotlightProvider } from '@mantine/spotlight';
|
||||
import { NotificationsProvider } from '@mantine/notifications';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { createSpotlightActions } from 'lib/spotlight';
|
||||
import { useRouter } from 'next/router';
|
||||
import { IconSearch } from '@tabler/icons-react';
|
||||
|
||||
export const themes = {
|
||||
system: (colorScheme: 'dark' | 'light') => (colorScheme === 'dark' ? dark_blue : light_blue),
|
||||
dark_blue,
|
||||
@@ -57,7 +52,6 @@ const cache = createEmotionCache({ key: 'zipline' });
|
||||
export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
||||
const user = useRecoilValue(userSelector);
|
||||
const colorScheme = useColorScheme();
|
||||
const router = useRouter();
|
||||
|
||||
let theme: MantineThemeOverride;
|
||||
|
||||
@@ -84,7 +78,7 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
||||
components: {
|
||||
AppShell: {
|
||||
styles: (t) => ({
|
||||
main: {
|
||||
root: {
|
||||
backgroundColor: t.other.AppShell_backgroundColor,
|
||||
},
|
||||
}),
|
||||
@@ -98,15 +92,10 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
||||
},
|
||||
Modal: {
|
||||
defaultProps: {
|
||||
closeButtonProps: { size: 'lg' },
|
||||
centered: true,
|
||||
transitionProps: {
|
||||
exitDuration: 100,
|
||||
},
|
||||
overlayProps: {
|
||||
blur: 6,
|
||||
color: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
|
||||
},
|
||||
overlayBlur: 3,
|
||||
overlayColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
|
||||
exitTransitionDuration: 100,
|
||||
},
|
||||
},
|
||||
Popover: {
|
||||
@@ -117,8 +106,8 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
||||
},
|
||||
LoadingOverlay: {
|
||||
defaultProps: {
|
||||
overlayBlur: 3,
|
||||
overlayColor: theme.colorScheme === 'dark' ? theme.colors.dark[6] : 'white',
|
||||
overlayOpacity: 0.3,
|
||||
},
|
||||
},
|
||||
Loader: {
|
||||
@@ -144,14 +133,9 @@ export default function ZiplineTheming({ Component, pageProps, ...props }) {
|
||||
}}
|
||||
>
|
||||
<ModalsProvider>
|
||||
<SpotlightProvider
|
||||
searchIcon={<IconSearch size='1rem' />}
|
||||
shortcut={['mod + k', '/']}
|
||||
actions={createSpotlightActions(router)}
|
||||
>
|
||||
<Notifications position='top-center' style={{ marginTop: -10 }} />
|
||||
<NotificationsProvider position='top-center' style={{ marginTop: -10 }}>
|
||||
{props.children ? props.children : <Component {...pageProps} />}
|
||||
</SpotlightProvider>
|
||||
</NotificationsProvider>
|
||||
</ModalsProvider>
|
||||
</MantineProvider>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import exts from 'lib/exts';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
@@ -10,17 +11,8 @@ import {
|
||||
Text,
|
||||
UnstyledButton,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
IconFile,
|
||||
IconFileAlert,
|
||||
IconFileText,
|
||||
IconFileUnknown,
|
||||
IconHeadphones,
|
||||
IconPhotoCancel,
|
||||
IconPlayerPlay,
|
||||
} from '@tabler/icons-react';
|
||||
import exts from 'lib/exts';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { AudioIcon, FileIcon, ImageIcon, PlayIcon } from './icons';
|
||||
import KaTeX from './render/KaTeX';
|
||||
import Markdown from './render/Markdown';
|
||||
import PrismCode from './render/PrismCode';
|
||||
@@ -45,8 +37,8 @@ function Placeholder({ text, Icon, ...props }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<Box sx={{ height: 320 }} {...props}>
|
||||
<Center sx={{ height: 320 }}>
|
||||
<Box sx={{ height: 200 }} {...props}>
|
||||
<Center sx={{ height: 200 }}>
|
||||
<PlaceholderContent text={text} Icon={Icon} />
|
||||
</Center>
|
||||
</Box>
|
||||
@@ -111,13 +103,13 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
|
||||
);
|
||||
|
||||
if (media && disableMediaPreview) {
|
||||
return <Placeholder Icon={IconFile} text={`Click to view file (${file.name})`} {...props} />;
|
||||
return <Placeholder Icon={FileIcon} text={`Click to view file (${file.name})`} {...props} />;
|
||||
}
|
||||
|
||||
if (file.password) {
|
||||
return (
|
||||
<Placeholder
|
||||
Icon={IconFileAlert}
|
||||
Icon={FileIcon}
|
||||
text={`This file is password protected. Click to view file (${file.name})`}
|
||||
onClick={() => window.open(file.url)}
|
||||
{...props}
|
||||
@@ -131,12 +123,7 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
|
||||
video: <video width='100%' autoPlay muted controls {...props} />,
|
||||
image: (
|
||||
<Image
|
||||
styles={{
|
||||
imageWrapper: {
|
||||
position: 'inherit',
|
||||
},
|
||||
}}
|
||||
placeholder={<PlaceholderContent Icon={IconPhotoCancel} text={'Image failed to load...'} />}
|
||||
placeholder={<PlaceholderContent Icon={FileIcon} text={'Image failed to load...'} />}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
@@ -159,19 +146,17 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
|
||||
)
|
||||
) : media ? (
|
||||
{
|
||||
video: <Placeholder Icon={IconPlayerPlay} text={`Click to view video (${file.name})`} {...props} />,
|
||||
video: <Placeholder Icon={PlayIcon} text={`Click to view video (${file.name})`} {...props} />,
|
||||
image: (
|
||||
<Image
|
||||
placeholder={<PlaceholderContent Icon={IconPhotoCancel} text={'Image failed to load...'} />}
|
||||
height={320}
|
||||
fit='contain'
|
||||
placeholder={<PlaceholderContent Icon={ImageIcon} text={'Image failed to load...'} />}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
audio: <Placeholder Icon={IconHeadphones} text={`Click to view audio (${file.name})`} {...props} />,
|
||||
text: <Placeholder Icon={IconFileText} text={`Click to view text file (${file.name})`} {...props} />,
|
||||
audio: <Placeholder Icon={AudioIcon} text={`Click to view audio (${file.name})`} {...props} />,
|
||||
text: <Placeholder Icon={FileIcon} text={`Click to view text file (${file.name})`} {...props} />,
|
||||
}[type]
|
||||
) : (
|
||||
<Placeholder Icon={IconFileUnknown} text={`Click to view file (${file.name})`} {...props} />
|
||||
<Placeholder Icon={FileIcon} text={`Click to view file (${file.name})`} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Box, Group, SimpleGrid, Text } from '@mantine/core';
|
||||
import { Box, Group, SimpleGrid, Text, useMantineTheme } from '@mantine/core';
|
||||
import { Dropzone as MantineDropzone } from '@mantine/dropzone';
|
||||
import { IconPhoto } from '@tabler/icons-react';
|
||||
import { ImageIcon } from 'components/icons';
|
||||
|
||||
export default function Dropzone({ loading, onDrop, children }) {
|
||||
const theme = useMantineTheme();
|
||||
|
||||
return (
|
||||
<SimpleGrid
|
||||
cols={2}
|
||||
@@ -11,9 +13,9 @@ export default function Dropzone({ loading, onDrop, children }) {
|
||||
{ maxWidth: 'xs', cols: 1 },
|
||||
]}
|
||||
>
|
||||
<MantineDropzone loading={loading} onDrop={onDrop} styles={{ inner: { pointerEvents: 'none' } }}>
|
||||
<MantineDropzone onDrop={onDrop} styles={{ inner: { pointerEvents: 'none' } }}>
|
||||
<Group position='center' spacing='xl' style={{ minHeight: 440 }}>
|
||||
<IconPhoto size={80} />
|
||||
<ImageIcon size={80} />
|
||||
|
||||
<Text size='xl' inline>
|
||||
Drag files here or click to select files
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ActionIcon, Box, Card, Group, HoverCard, Table, useMantineTheme } from '@mantine/core';
|
||||
import { IconX } from '@tabler/icons-react';
|
||||
import { ActionIcon, Badge, Box, Card, Group, HoverCard, Table, useMantineTheme } from '@mantine/core';
|
||||
import Type from 'components/Type';
|
||||
import { X } from 'react-feather';
|
||||
|
||||
export function FilePreview({ file }: { file: File }) {
|
||||
return (
|
||||
@@ -23,6 +23,7 @@ export default function FileDropzone({ file, onRemove }: { file: File; onRemove:
|
||||
return (
|
||||
<HoverCard shadow='md'>
|
||||
<HoverCard.Target>
|
||||
{/* <Badge size='lg'>{file.name}</Badge> */}
|
||||
<Card shadow='sm' radius='sm' p='sm'>
|
||||
<Group position='center' spacing='xl'>
|
||||
{file.name}
|
||||
@@ -30,6 +31,7 @@ export default function FileDropzone({ file, onRemove }: { file: File; onRemove:
|
||||
</Card>
|
||||
</HoverCard.Target>
|
||||
<HoverCard.Dropdown>
|
||||
{/* x button that will remove file */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
@@ -41,7 +43,7 @@ export default function FileDropzone({ file, onRemove }: { file: File; onRemove:
|
||||
m='xs'
|
||||
>
|
||||
<ActionIcon onClick={onRemove} size='sm' color='red' variant='filled'>
|
||||
<IconX />
|
||||
<X />
|
||||
</ActionIcon>
|
||||
</Box>
|
||||
|
||||
|
||||
5
src/components/icons/ActivityIcon.tsx
Normal file
5
src/components/icons/ActivityIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Activity } from 'react-feather';
|
||||
|
||||
export default function ActivityIcon({ ...props }) {
|
||||
return <Activity size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/AudioIcon.tsx
Normal file
5
src/components/icons/AudioIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Disc } from 'react-feather';
|
||||
|
||||
export default function AudioIcon({ ...props }) {
|
||||
return <Disc size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/CalendarIcon.tsx
Normal file
5
src/components/icons/CalendarIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Calendar } from 'react-feather';
|
||||
|
||||
export default function CalendarIcon({ ...props }) {
|
||||
return <Calendar size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/CheckIcon.tsx
Normal file
5
src/components/icons/CheckIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Check } from 'react-feather';
|
||||
|
||||
export default function CheckIcon({ ...props }) {
|
||||
return <Check size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/ClockIcon.tsx
Normal file
5
src/components/icons/ClockIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Clock } from 'react-feather';
|
||||
|
||||
export default function ClockIcon({ ...props }) {
|
||||
return <Clock size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/CopyIcon.tsx
Normal file
5
src/components/icons/CopyIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Copy } from 'react-feather';
|
||||
|
||||
export default function CopyIcon({ ...props }) {
|
||||
return <Copy size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/CrossIcon.tsx
Normal file
5
src/components/icons/CrossIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { X } from 'react-feather';
|
||||
|
||||
export default function CrossIcon({ ...props }) {
|
||||
return <X size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/DatabaseIcon.tsx
Normal file
5
src/components/icons/DatabaseIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Database } from 'react-feather';
|
||||
|
||||
export default function DatabaseIcon({ ...props }) {
|
||||
return <Database size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/DeleteIcon.tsx
Normal file
5
src/components/icons/DeleteIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Delete } from 'react-feather';
|
||||
|
||||
export default function DeleteIcon({ ...props }) {
|
||||
return <Delete size={15} {...props} />;
|
||||
}
|
||||
19
src/components/icons/DiscordIcon.tsx
Normal file
19
src/components/icons/DiscordIcon.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
// https://discord.com/branding
|
||||
|
||||
export default function DiscordIcon({ ...props }) {
|
||||
return (
|
||||
<svg width='24' height='24' viewBox='0 0 71 55' xmlns='http://www.w3.org/2000/svg'>
|
||||
<g clipPath='url(#clip0)'>
|
||||
<path
|
||||
fill={props.colorScheme === 'manage' ? '#ffffff' : '#5865F2'}
|
||||
d='M60.1045 4.8978C55.5792 2.8214 50.7265 1.2916 45.6527 0.41542C45.5603 0.39851 45.468 0.440769 45.4204 0.525289C44.7963 1.6353 44.105 3.0834 43.6209 4.2216C38.1637 3.4046 32.7345 3.4046 27.3892 4.2216C26.905 3.0581 26.1886 1.6353 25.5617 0.525289C25.5141 0.443589 25.4218 0.40133 25.3294 0.41542C20.2584 1.2888 15.4057 2.8186 10.8776 4.8978C10.8384 4.9147 10.8048 4.9429 10.7825 4.9795C1.57795 18.7309 -0.943561 32.1443 0.293408 45.3914C0.299005 45.4562 0.335386 45.5182 0.385761 45.5576C6.45866 50.0174 12.3413 52.7249 18.1147 54.5195C18.2071 54.5477 18.305 54.5139 18.3638 54.4378C19.7295 52.5728 20.9469 50.6063 21.9907 48.5383C22.0523 48.4172 21.9935 48.2735 21.8676 48.2256C19.9366 47.4931 18.0979 46.6 16.3292 45.5858C16.1893 45.5041 16.1781 45.304 16.3068 45.2082C16.679 44.9293 17.0513 44.6391 17.4067 44.3461C17.471 44.2926 17.5606 44.2813 17.6362 44.3151C29.2558 49.6202 41.8354 49.6202 53.3179 44.3151C53.3935 44.2785 53.4831 44.2898 53.5502 44.3433C53.9057 44.6363 54.2779 44.9293 54.6529 45.2082C54.7816 45.304 54.7732 45.5041 54.6333 45.5858C52.8646 46.6197 51.0259 47.4931 49.0921 48.2228C48.9662 48.2707 48.9102 48.4172 48.9718 48.5383C50.038 50.6034 51.2554 52.5699 52.5959 54.435C52.6519 54.5139 52.7526 54.5477 52.845 54.5195C58.6464 52.7249 64.529 50.0174 70.6019 45.5576C70.6551 45.5182 70.6887 45.459 70.6943 45.3942C72.1747 30.0791 68.2147 16.7757 60.1968 4.9823C60.1772 4.9429 60.1437 4.9147 60.1045 4.8978ZM23.7259 37.3253C20.2276 37.3253 17.3451 34.1136 17.3451 30.1693C17.3451 26.225 20.1717 23.0133 23.7259 23.0133C27.308 23.0133 30.1626 26.2532 30.1066 30.1693C30.1066 34.1136 27.28 37.3253 23.7259 37.3253ZM47.3178 37.3253C43.8196 37.3253 40.9371 34.1136 40.9371 30.1693C40.9371 26.225 43.7636 23.0133 47.3178 23.0133C50.9 23.0133 53.7545 26.2532 53.6986 30.1693C53.6986 34.1136 50.9 37.3253 47.3178 37.3253Z'
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id='clip0'>
|
||||
<rect width='71' height='55' fill='white' />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
5
src/components/icons/DownloadIcon.tsx
Normal file
5
src/components/icons/DownloadIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Download } from 'react-feather';
|
||||
|
||||
export default function DownloadIcon({ ...props }) {
|
||||
return <Download size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/EnterIcon.tsx
Normal file
5
src/components/icons/EnterIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { LogIn } from 'react-feather';
|
||||
|
||||
export default function EnterIcon({ ...props }) {
|
||||
return <LogIn size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/ExternalLinkIcon.tsx
Normal file
5
src/components/icons/ExternalLinkIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ExternalLink } from 'react-feather';
|
||||
|
||||
export default function ExternalLinkIcon({ ...props }) {
|
||||
return <ExternalLink size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/EyeIcon.tsx
Normal file
5
src/components/icons/EyeIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Eye } from 'react-feather';
|
||||
|
||||
export default function EyeIcon({ ...props }) {
|
||||
return <Eye size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/FileIcon.tsx
Normal file
5
src/components/icons/FileIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { File } from 'react-feather';
|
||||
|
||||
export default function FileIcon({ ...props }) {
|
||||
return <File size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/FolderIcon.tsx
Normal file
5
src/components/icons/FolderIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Folder } from 'react-feather';
|
||||
|
||||
export default function FolderIcon({ ...props }) {
|
||||
return <Folder size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/FolderMinusIcon.tsx
Normal file
5
src/components/icons/FolderMinusIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { FolderMinus } from 'react-feather';
|
||||
|
||||
export default function FolderMinusIcon({ ...props }) {
|
||||
return <FolderMinus size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/FolderPlusIcon.tsx
Normal file
5
src/components/icons/FolderPlusIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { FolderPlus } from 'react-feather';
|
||||
|
||||
export default function FolderPlusIcon({ ...props }) {
|
||||
return <FolderPlus size={15} {...props} />;
|
||||
}
|
||||
17
src/components/icons/GitHubIcon.tsx
Normal file
17
src/components/icons/GitHubIcon.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { GitHub } from 'react-feather';
|
||||
import Image from 'next/image';
|
||||
|
||||
// https://upload.wikimedia.org/wikipedia/commons/9/91/Octicons-mark-github.svg
|
||||
export default function GitHubIcon({ colorScheme, ...props }) {
|
||||
return (
|
||||
<svg width={24} height={24} viewBox='0 0 1024 1024' xmlns='http://www.w3.org/2000/svg' {...props}>
|
||||
<path
|
||||
fillRule='evenodd'
|
||||
clipRule='evenodd'
|
||||
d='M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z'
|
||||
transform='scale(64)'
|
||||
fill={colorScheme === 'dark' ? '#FFFFFF' : '#1B1F23'}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
5
src/components/icons/GlobeIcon.tsx
Normal file
5
src/components/icons/GlobeIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Globe } from 'react-feather';
|
||||
|
||||
export default function GlobeIcon({ ...props }) {
|
||||
return <Globe size={15} {...props} />;
|
||||
}
|
||||
15
src/components/icons/GoogleIcon.tsx
Normal file
15
src/components/icons/GoogleIcon.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
// https://developers.google.com/identity/branding-guidelines
|
||||
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function GoogleIcon({ colorScheme, ...props }) {
|
||||
return (
|
||||
<Image
|
||||
alt='google'
|
||||
src='https://madeby.google.com/static/images/google_g_logo.svg'
|
||||
width={24}
|
||||
height={24}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
5
src/components/icons/HardDriveIcon.tsx
Normal file
5
src/components/icons/HardDriveIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { HardDrive } from 'react-feather';
|
||||
|
||||
export default function HardDriveIcon({ ...props }) {
|
||||
return <HardDrive size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/HashIcon.tsx
Normal file
5
src/components/icons/HashIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Hash } from 'react-feather';
|
||||
|
||||
export default function HashIcon({ ...props }) {
|
||||
return <Hash size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/HomeIcon.tsx
Normal file
5
src/components/icons/HomeIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Home } from 'react-feather';
|
||||
|
||||
export default function HomeIcon({ ...props }) {
|
||||
return <Home size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/ImageIcon.tsx
Normal file
5
src/components/icons/ImageIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Image as FeatherImage } from 'react-feather';
|
||||
|
||||
export default function ImageIcon({ ...props }) {
|
||||
return <FeatherImage size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/InfoIcon.tsx
Normal file
5
src/components/icons/InfoIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Info } from 'react-feather';
|
||||
|
||||
export default function InfoIcon({ ...props }) {
|
||||
return <Info size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/KeyIcon.tsx
Normal file
5
src/components/icons/KeyIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Key } from 'react-feather';
|
||||
|
||||
export default function KeyIcon({ ...props }) {
|
||||
return <Key size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/LinkIcon.tsx
Normal file
5
src/components/icons/LinkIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Link } from 'react-feather';
|
||||
|
||||
export default function LinkIcon({ ...props }) {
|
||||
return <Link size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/LockIcon.tsx
Normal file
5
src/components/icons/LockIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Lock } from 'react-feather';
|
||||
|
||||
export default function LockIcon({ ...props }) {
|
||||
return <Lock size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/LogoutIcon.tsx
Normal file
5
src/components/icons/LogoutIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { LogOut } from 'react-feather';
|
||||
|
||||
export default function LogoutIcon({ ...props }) {
|
||||
return <LogOut size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/PencilIcon.tsx
Normal file
5
src/components/icons/PencilIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Edit2 } from 'react-feather';
|
||||
|
||||
export default function PencilIcon({ ...props }) {
|
||||
return <Edit2 size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/PlayIcon.tsx
Normal file
5
src/components/icons/PlayIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Play } from 'react-feather';
|
||||
|
||||
export default function PlayIcon({ ...props }) {
|
||||
return <Play size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/PlusIcon.tsx
Normal file
5
src/components/icons/PlusIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Plus } from 'react-feather';
|
||||
|
||||
export default function PlusIcon({ ...props }) {
|
||||
return <Plus size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/RefreshIcon.tsx
Normal file
5
src/components/icons/RefreshIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { RefreshCw } from 'react-feather';
|
||||
|
||||
export default function RefreshIcon({ ...props }) {
|
||||
return <RefreshCw size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/SettingsIcon.tsx
Normal file
5
src/components/icons/SettingsIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Settings } from 'react-feather';
|
||||
|
||||
export default function SettingsIcon({ ...props }) {
|
||||
return <Settings size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/StarIcon.tsx
Normal file
5
src/components/icons/StarIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Star } from 'react-feather';
|
||||
|
||||
export default function StarIcon({ ...props }) {
|
||||
return <Star size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/TagIcon.tsx
Normal file
5
src/components/icons/TagIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Tag } from 'react-feather';
|
||||
|
||||
export default function TagIcon({ ...props }) {
|
||||
return <Tag size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/TrashIcon.tsx
Normal file
5
src/components/icons/TrashIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Trash2 } from 'react-feather';
|
||||
|
||||
export default function TrashIcon({ ...props }) {
|
||||
return <Trash2 size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/TypeIcon.tsx
Normal file
5
src/components/icons/TypeIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Type } from 'react-feather';
|
||||
|
||||
export default function TypeIcon({ ...props }) {
|
||||
return <Type size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/UnlockIcon.tsx
Normal file
5
src/components/icons/UnlockIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Unlock } from 'react-feather';
|
||||
|
||||
export default function UnlockIcon({ ...props }) {
|
||||
return <Unlock size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/UploadIcon.tsx
Normal file
5
src/components/icons/UploadIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Upload } from 'react-feather';
|
||||
|
||||
export default function UploadIcon({ ...props }) {
|
||||
return <Upload size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/UserIcon.tsx
Normal file
5
src/components/icons/UserIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { User } from 'react-feather';
|
||||
|
||||
export default function UserIcon({ ...props }) {
|
||||
return <User size={15} {...props} />;
|
||||
}
|
||||
5
src/components/icons/VideoIcon.tsx
Normal file
5
src/components/icons/VideoIcon.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Video } from 'react-feather';
|
||||
|
||||
export default function VideoIcon({ ...props }) {
|
||||
return <Video size={15} {...props} />;
|
||||
}
|
||||
@@ -1,4 +1,91 @@
|
||||
import ActivityIcon from './ActivityIcon';
|
||||
import CheckIcon from './CheckIcon';
|
||||
import CopyIcon from './CopyIcon';
|
||||
import CrossIcon from './CrossIcon';
|
||||
import DeleteIcon from './DeleteIcon';
|
||||
import FileIcon from './FileIcon';
|
||||
import HomeIcon from './HomeIcon';
|
||||
import LinkIcon from './LinkIcon';
|
||||
import LogoutIcon from './LogoutIcon';
|
||||
import PencilIcon from './PencilIcon';
|
||||
import SettingsIcon from './SettingsIcon';
|
||||
import TypeIcon from './TypeIcon';
|
||||
import UploadIcon from './UploadIcon';
|
||||
import UserIcon from './UserIcon';
|
||||
import EnterIcon from './EnterIcon';
|
||||
import PlusIcon from './PlusIcon';
|
||||
import ImageIcon from './ImageIcon';
|
||||
import StarIcon from './StarIcon';
|
||||
import AudioIcon from './AudioIcon';
|
||||
import VideoIcon from './VideoIcon';
|
||||
import PlayIcon from './PlayIcon';
|
||||
import CalendarIcon from './CalendarIcon';
|
||||
import HashIcon from './HashIcon';
|
||||
import TagIcon from './TagIcon';
|
||||
import ClockIcon from './ClockIcon';
|
||||
import ExternalLinkIcon from './ExternalLinkIcon';
|
||||
import ShareXIcon from './ShareXIcon';
|
||||
import DownloadIcon from './DownloadIcon';
|
||||
import FlameshotIcon from './FlameshotIcon';
|
||||
import GitHubIcon from './GitHubIcon';
|
||||
import DiscordIcon from './DiscordIcon';
|
||||
import GoogleIcon from './GoogleIcon';
|
||||
import EyeIcon from './EyeIcon';
|
||||
import RefreshIcon from './RefreshIcon';
|
||||
import KeyIcon from './KeyIcon';
|
||||
import DatabaseIcon from './DatabaseIcon';
|
||||
import InfoIcon from './InfoIcon';
|
||||
import FolderIcon from './FolderIcon';
|
||||
import FolderMinusIcon from './FolderMinusIcon';
|
||||
import FolderPlusIcon from './FolderPlusIcon';
|
||||
import GlobeIcon from './GlobeIcon';
|
||||
import LockIcon from './LockIcon';
|
||||
import UnlockIcon from './UnlockIcon';
|
||||
import HardDriveIcon from './HardDriveIcon';
|
||||
|
||||
export { ShareXIcon, FlameshotIcon };
|
||||
export {
|
||||
ActivityIcon,
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
CrossIcon,
|
||||
DeleteIcon,
|
||||
FileIcon,
|
||||
HomeIcon,
|
||||
LinkIcon,
|
||||
LogoutIcon,
|
||||
PencilIcon,
|
||||
SettingsIcon,
|
||||
TypeIcon,
|
||||
UploadIcon,
|
||||
UserIcon,
|
||||
EnterIcon,
|
||||
PlusIcon,
|
||||
ImageIcon,
|
||||
StarIcon,
|
||||
AudioIcon,
|
||||
VideoIcon,
|
||||
PlayIcon,
|
||||
CalendarIcon,
|
||||
HashIcon,
|
||||
TagIcon,
|
||||
ClockIcon,
|
||||
ExternalLinkIcon,
|
||||
ShareXIcon,
|
||||
DownloadIcon,
|
||||
FlameshotIcon,
|
||||
GitHubIcon,
|
||||
DiscordIcon,
|
||||
GoogleIcon,
|
||||
EyeIcon,
|
||||
RefreshIcon,
|
||||
KeyIcon,
|
||||
DatabaseIcon,
|
||||
InfoIcon,
|
||||
FolderIcon,
|
||||
FolderMinusIcon,
|
||||
FolderPlusIcon,
|
||||
GlobeIcon,
|
||||
LockIcon,
|
||||
UnlockIcon,
|
||||
HardDriveIcon,
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Card as MantineCard, Center, Group, SimpleGrid, Skeleton, Title } from '@mantine/core';
|
||||
import { randomId } from '@mantine/hooks';
|
||||
import { IconCloudUpload } from '@tabler/icons-react';
|
||||
import File from 'components/File';
|
||||
import MutedText from 'components/MutedText';
|
||||
import { useRecent } from 'lib/queries/files';
|
||||
import { UploadCloud } from 'react-feather';
|
||||
|
||||
export default function RecentFiles({ disableMediaPreview, exifEnabled, compress }) {
|
||||
export default function RecentFiles({ disableMediaPreview, exifEnabled }) {
|
||||
const recent = useRecent('media');
|
||||
|
||||
return (
|
||||
@@ -25,7 +25,6 @@ export default function RecentFiles({ disableMediaPreview, exifEnabled, compress
|
||||
disableMediaPreview={disableMediaPreview}
|
||||
exifEnabled={exifEnabled}
|
||||
refreshImages={recent.refetch}
|
||||
onDash={compress}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
@@ -33,7 +32,7 @@ export default function RecentFiles({ disableMediaPreview, exifEnabled, compress
|
||||
<Center>
|
||||
<Group>
|
||||
<div>
|
||||
<IconCloudUpload size={48} />
|
||||
<UploadCloud size={48} />
|
||||
</div>
|
||||
<div>
|
||||
<Title>Nothing here</Title>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SimpleGrid } from '@mantine/core';
|
||||
import { IconDatabase, IconEye, IconFile, IconUsers } from '@tabler/icons-react';
|
||||
import StatCard from 'components/StatCard';
|
||||
import { useStats } from 'lib/queries/stats';
|
||||
import { percentChange } from 'lib/utils/client';
|
||||
import { EyeIcon, DatabaseIcon, UserIcon, FileIcon } from 'components/icons';
|
||||
|
||||
export function StatCards() {
|
||||
const stats = useStats();
|
||||
@@ -23,7 +23,7 @@ export function StatCards() {
|
||||
title: 'FILES',
|
||||
value: stats.isSuccess ? latest.data.count.toLocaleString() : '...',
|
||||
desc: 'files have been uploaded',
|
||||
icon: <IconFile />,
|
||||
icon: <FileIcon />,
|
||||
diff:
|
||||
stats.isSuccess && before?.data ? percentChange(before.data.count, latest.data.count) : undefined,
|
||||
}}
|
||||
@@ -34,7 +34,7 @@ export function StatCards() {
|
||||
title: 'STORAGE',
|
||||
value: stats.isSuccess ? latest.data.size : '...',
|
||||
desc: 'used',
|
||||
icon: <IconDatabase />,
|
||||
icon: <DatabaseIcon />,
|
||||
diff:
|
||||
stats.isSuccess && before?.data
|
||||
? percentChange(before.data.size_num, latest.data.size_num)
|
||||
@@ -47,7 +47,7 @@ export function StatCards() {
|
||||
title: 'VIEWS',
|
||||
value: stats.isSuccess ? latest.data.views_count.toLocaleString() : '...',
|
||||
desc: 'total file views',
|
||||
icon: <IconEye />,
|
||||
icon: <EyeIcon />,
|
||||
diff:
|
||||
stats.isSuccess && before?.data
|
||||
? percentChange(before.data.views_count, latest.data.views_count)
|
||||
@@ -60,7 +60,7 @@ export function StatCards() {
|
||||
title: 'USERS',
|
||||
value: stats.isSuccess ? latest.data.count_users.toLocaleString() : '...',
|
||||
desc: 'users',
|
||||
icon: <IconUsers />,
|
||||
icon: <UserIcon />,
|
||||
}}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
import { ActionIcon, Box, Group, Title, Tooltip } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconClipboardCopy,
|
||||
IconExternalLink,
|
||||
IconGridDots,
|
||||
IconPhotoCancel,
|
||||
IconPhotoMinus,
|
||||
IconPhotoUp,
|
||||
} from '@tabler/icons-react';
|
||||
import FileModal from 'components/File/FileModal';
|
||||
import { CopyIcon, CrossIcon, DeleteIcon, EnterIcon, FileIcon } from 'components/icons';
|
||||
import Link from 'components/Link';
|
||||
import MutedText from 'components/MutedText';
|
||||
import useFetch from 'lib/hooks/useFetch';
|
||||
import { usePaginatedFiles, useRecent } from 'lib/queries/files';
|
||||
import { useStats } from 'lib/queries/stats';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
import { bytesToHuman } from 'lib/utils/bytes';
|
||||
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import RecentFiles from './RecentFiles';
|
||||
import { StatCards } from './StatCards';
|
||||
|
||||
export default function Dashboard({ disableMediaPreview, exifEnabled, compress }) {
|
||||
export default function Dashboard({ disableMediaPreview, exifEnabled }) {
|
||||
const user = useRecoilValue(userSelector);
|
||||
|
||||
const recent = useRecent('media');
|
||||
@@ -45,7 +37,7 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
|
||||
})();
|
||||
}, [page]);
|
||||
|
||||
const files = usePaginatedFiles(page, 'none');
|
||||
const files = usePaginatedFiles(page);
|
||||
|
||||
// sorting
|
||||
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
|
||||
@@ -92,14 +84,14 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
|
||||
title: 'File Deleted',
|
||||
message: `${file.name}`,
|
||||
color: 'green',
|
||||
icon: <IconPhotoMinus size='1rem' />,
|
||||
icon: <DeleteIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Failed to Delete File',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <IconPhotoCancel size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -120,7 +112,7 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
|
||||
href={`${window.location.protocol}//${window.location.host}${file.url}`}
|
||||
>{`${window.location.protocol}//${window.location.host}${file.url}`}</a>
|
||||
),
|
||||
icon: <IconClipboardCopy size='1rem' />,
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -139,7 +131,6 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
|
||||
refresh={() => files.refetch()}
|
||||
reducedActions={false}
|
||||
exifEnabled={exifEnabled}
|
||||
compress={compress}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -150,17 +141,13 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
|
||||
|
||||
<StatCards />
|
||||
|
||||
<RecentFiles disableMediaPreview={disableMediaPreview} exifEnabled={exifEnabled} compress={compress} />
|
||||
<RecentFiles disableMediaPreview={disableMediaPreview} exifEnabled={exifEnabled} />
|
||||
|
||||
<Box my='sm'>
|
||||
<Group mb='md'>
|
||||
<Title>Files</Title>
|
||||
<Tooltip label='View Gallery'>
|
||||
<ActionIcon variant='filled' color='primary' component={Link} href='/dashboard/files'>
|
||||
<IconGridDots size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
<Title>Files</Title>
|
||||
<MutedText size='md'>
|
||||
View your gallery <Link href='/dashboard/files'>here</Link>.
|
||||
</MutedText>
|
||||
|
||||
<DataTable
|
||||
withBorder
|
||||
@@ -170,7 +157,6 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
|
||||
columns={[
|
||||
{ accessor: 'name', sortable: true },
|
||||
{ accessor: 'mimetype', sortable: true },
|
||||
{ accessor: 'size', sortable: true, render: (file) => bytesToHuman(file.size) },
|
||||
{
|
||||
accessor: 'createdAt',
|
||||
sortable: true,
|
||||
@@ -189,21 +175,21 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
|
||||
}}
|
||||
color='blue'
|
||||
>
|
||||
<IconPhotoUp size='1rem' />
|
||||
<FileIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label='Open file in new tab'>
|
||||
<ActionIcon onClick={() => viewFile(file)} color='blue'>
|
||||
<IconExternalLink size='1rem' />
|
||||
<EnterIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<ActionIcon onClick={() => copyFile(file)} color='green'>
|
||||
<IconClipboardCopy size='1rem' />
|
||||
<CopyIcon />
|
||||
</ActionIcon>
|
||||
<ActionIcon onClick={() => deleteFile(file)} color='red'>
|
||||
<IconPhotoMinus size='1rem' />
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
),
|
||||
@@ -226,27 +212,25 @@ export default function Dashboard({ disableMediaPreview, exifEnabled, compress }
|
||||
items: (file) => [
|
||||
{
|
||||
key: 'view',
|
||||
icon: <IconExternalLink size='1rem' />,
|
||||
icon: <EnterIcon />,
|
||||
title: `View ${file.name}`,
|
||||
onClick: () => viewFile(file),
|
||||
},
|
||||
{
|
||||
key: 'copy',
|
||||
icon: <IconClipboardCopy size='1rem' />,
|
||||
icon: <CopyIcon />,
|
||||
title: `Copy ${file.name}`,
|
||||
onClick: () => copyFile(file),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
icon: <IconPhotoMinus size='1rem' />,
|
||||
icon: <DeleteIcon />,
|
||||
title: `Delete ${file.name}`,
|
||||
onClick: () => deleteFile(file),
|
||||
},
|
||||
],
|
||||
}}
|
||||
onCellClick={({ column, record: file }) => {
|
||||
if (column.accessor === 'actions') return;
|
||||
|
||||
onCellClick={({ record: file }) => {
|
||||
setSelectedFile(file);
|
||||
setOpen(true);
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Button, Center, Checkbox, Group, Pagination, SimpleGrid, Skeleton, Title } from '@mantine/core';
|
||||
import { useMediaQuery } from '@mantine/hooks';
|
||||
import { IconFile } from '@tabler/icons-react';
|
||||
import File from 'components/File';
|
||||
import { FileIcon } from 'components/icons';
|
||||
import MutedText from 'components/MutedText';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { usePaginatedFiles } from 'lib/queries/files';
|
||||
@@ -10,7 +10,7 @@ import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
export default function FilePagation({ disableMediaPreview, exifEnabled, queryPage, compress }) {
|
||||
export default function FilePagation({ disableMediaPreview, exifEnabled, queryPage }) {
|
||||
const [checked, setChecked] = useRecoilState(showNonMediaSelector);
|
||||
const [numPages, setNumPages] = useState(Number(queryPage)); // just set it to the queryPage, since the req may have not loaded yet
|
||||
const [page, setPage] = useState(Number(queryPage));
|
||||
@@ -44,7 +44,7 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa
|
||||
<Center sx={{ flexDirection: 'column' }}>
|
||||
<Group>
|
||||
<div>
|
||||
<IconFile size={48} />
|
||||
<FileIcon size={48} />
|
||||
</div>
|
||||
<div>
|
||||
<Title>Nothing here</Title>
|
||||
@@ -75,7 +75,6 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa
|
||||
disableMediaPreview={disableMediaPreview}
|
||||
exifEnabled={exifEnabled}
|
||||
refreshImages={pages.refetch}
|
||||
onDash={compress}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
@@ -97,7 +96,7 @@ export default function FilePagation({ disableMediaPreview, exifEnabled, queryPa
|
||||
}}
|
||||
>
|
||||
{!isMobile && <div></div>}
|
||||
<Pagination total={numPages} value={page} onChange={setPage} withEdges />
|
||||
<Pagination total={numPages} page={page} onChange={setPage} withEdges />
|
||||
{!isMobile && (
|
||||
<Checkbox
|
||||
label='Show non-media files'
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Accordion, ActionIcon, Box, Group, Pagination, SimpleGrid, Title } from '@mantine/core';
|
||||
import { IconFileUpload } from '@tabler/icons-react';
|
||||
import File from 'components/File';
|
||||
import { PlusIcon } from 'components/icons';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { usePaginatedFiles } from 'lib/queries/files';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import FilePagation from './FilePagation';
|
||||
|
||||
export default function Files({ disableMediaPreview, exifEnabled, queryPage, compress }) {
|
||||
export default function Files({ disableMediaPreview, exifEnabled, queryPage }) {
|
||||
const [favoritePage, setFavoritePage] = useState(1);
|
||||
const [favoriteNumPages, setFavoriteNumPages] = useState(0);
|
||||
const favoritePages = usePaginatedFiles(favoritePage, 'media', true);
|
||||
@@ -23,9 +23,11 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage, com
|
||||
<>
|
||||
<Group mb='md'>
|
||||
<Title>Files</Title>
|
||||
<ActionIcon component={Link} href='/dashboard/upload/file' variant='filled' color='primary'>
|
||||
<IconFileUpload size='1rem' />
|
||||
</ActionIcon>
|
||||
<Link href='/dashboard/upload/file' passHref legacyBehavior>
|
||||
<ActionIcon component='a' variant='filled' color='primary'>
|
||||
<PlusIcon />
|
||||
</ActionIcon>
|
||||
</Link>
|
||||
</Group>
|
||||
{favoritePages.isSuccess && favoritePages.data.length ? (
|
||||
<Accordion
|
||||
@@ -48,7 +50,6 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage, com
|
||||
disableMediaPreview={disableMediaPreview}
|
||||
exifEnabled={exifEnabled}
|
||||
refreshImages={favoritePages.refetch}
|
||||
onDash={compress}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
@@ -63,7 +64,7 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage, com
|
||||
paddingBottom: 3,
|
||||
}}
|
||||
>
|
||||
<Pagination total={favoriteNumPages} value={favoritePage} onChange={setFavoritePage} />
|
||||
<Pagination total={favoriteNumPages} page={favoritePage} onChange={setFavoritePage} />
|
||||
</Box>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
@@ -74,7 +75,6 @@ export default function Files({ disableMediaPreview, exifEnabled, queryPage, com
|
||||
disableMediaPreview={disableMediaPreview}
|
||||
exifEnabled={exifEnabled}
|
||||
queryPage={queryPage}
|
||||
compress={compress}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, Group, Modal, TextInput, Title } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconFolderPlus, IconFolderX } from '@tabler/icons-react';
|
||||
import { CrossIcon, FolderIcon } from 'components/icons';
|
||||
import MutedText from 'components/MutedText';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useRouter } from 'next/router';
|
||||
@@ -25,14 +25,14 @@ export default function CreateFolderModal({ open, setOpen, updateFolders, create
|
||||
showNotification({
|
||||
title: 'Failed to create folder',
|
||||
message: res.error,
|
||||
icon: <IconFolderX size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
color: 'red',
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Created folder ' + res.name,
|
||||
message: createWithFile ? 'Added file to folder' : undefined,
|
||||
icon: <IconFolderPlus size='1rem' />,
|
||||
icon: <FolderIcon />,
|
||||
color: 'green',
|
||||
});
|
||||
|
||||
@@ -43,7 +43,6 @@ export default function CreateFolderModal({ open, setOpen, updateFolders, create
|
||||
|
||||
setOpen(false);
|
||||
updateFolders();
|
||||
form.setValues({ name: '' });
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,14 +3,7 @@ import File from 'components/File';
|
||||
import MutedText from 'components/MutedText';
|
||||
import { useFolder } from 'lib/queries/folders';
|
||||
|
||||
export default function ViewFolderFilesModal({
|
||||
open,
|
||||
setOpen,
|
||||
folderId,
|
||||
disableMediaPreview,
|
||||
exifEnabled,
|
||||
compress,
|
||||
}) {
|
||||
export default function ViewFolderFilesModal({ open, setOpen, folderId, disableMediaPreview, exifEnabled }) {
|
||||
if (!folderId) return null;
|
||||
|
||||
const folder = useFolder(folderId, true);
|
||||
@@ -33,7 +26,6 @@ export default function ViewFolderFilesModal({
|
||||
image={file}
|
||||
exifEnabled={exifEnabled}
|
||||
refreshImages={folder.refetch}
|
||||
onDash={compress}
|
||||
/>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
||||
@@ -2,36 +2,18 @@ import { ActionIcon, Avatar, Card, Group, SimpleGrid, Skeleton, Stack, Title, To
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconClipboardCheck,
|
||||
IconClipboardCopy,
|
||||
IconExternalLink,
|
||||
IconFiles,
|
||||
IconFolderCancel,
|
||||
IconFolderMinus,
|
||||
IconFolderPlus,
|
||||
IconFolderShare,
|
||||
IconFolderX,
|
||||
IconGridDots,
|
||||
IconList,
|
||||
IconLock,
|
||||
IconLockCancel,
|
||||
IconLockOpen,
|
||||
} from '@tabler/icons-react';
|
||||
import AnchorNext from 'components/AnchorNext';
|
||||
import { DeleteIcon, FileIcon, PlusIcon, LockIcon, UnlockIcon, LinkIcon, CopyIcon } from 'components/icons';
|
||||
import Link from 'components/Link';
|
||||
import MutedText from 'components/MutedText';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useFolders } from 'lib/queries/folders';
|
||||
import { listViewFoldersSelector } from 'lib/recoil/settings';
|
||||
import { relativeTime } from 'lib/utils/client';
|
||||
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import CreateFolderModal from './CreateFolderModal';
|
||||
import ViewFolderFilesModal from './ViewFolderFilesModal';
|
||||
|
||||
export default function Folders({ disableMediaPreview, exifEnabled, compress }) {
|
||||
export default function Folders({ disableMediaPreview, exifEnabled }) {
|
||||
const folders = useFolders();
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [createWithFile, setCreateWithFile] = useState(null);
|
||||
@@ -42,32 +24,6 @@ export default function Folders({ disableMediaPreview, exifEnabled, compress })
|
||||
const clipboard = useClipboard();
|
||||
const router = useRouter();
|
||||
|
||||
const [listView, setListView] = useRecoilState(listViewFoldersSelector);
|
||||
|
||||
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
|
||||
columnAccessor: 'updatedAt',
|
||||
direction: 'desc',
|
||||
});
|
||||
const [records, setRecords] = useState(folders.data);
|
||||
|
||||
useEffect(() => {
|
||||
setRecords(folders.data);
|
||||
}, [folders.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!records || records.length === 0) return;
|
||||
|
||||
const sortedRecords = [...records].sort((a, b) => {
|
||||
if (sortStatus.direction === 'asc') {
|
||||
return a[sortStatus.columnAccessor] > b[sortStatus.columnAccessor] ? 1 : -1;
|
||||
}
|
||||
|
||||
return a[sortStatus.columnAccessor] < b[sortStatus.columnAccessor] ? 1 : -1;
|
||||
});
|
||||
|
||||
setRecords(sortedRecords);
|
||||
}, [sortStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (router.query.create) {
|
||||
setCreateOpen(true);
|
||||
@@ -94,7 +50,7 @@ export default function Folders({ disableMediaPreview, exifEnabled, compress })
|
||||
title: 'Deleted folder',
|
||||
message: `Deleted folder ${folder.name}`,
|
||||
color: 'green',
|
||||
icon: <IconFolderMinus size='1rem' />,
|
||||
icon: <DeleteIcon />,
|
||||
});
|
||||
folders.refetch();
|
||||
} else {
|
||||
@@ -102,7 +58,7 @@ export default function Folders({ disableMediaPreview, exifEnabled, compress })
|
||||
title: 'Failed to delete folder',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <IconFolderCancel size='1rem' />,
|
||||
icon: <DeleteIcon />,
|
||||
});
|
||||
folders.refetch();
|
||||
}
|
||||
@@ -120,7 +76,7 @@ export default function Folders({ disableMediaPreview, exifEnabled, compress })
|
||||
title: 'Made folder public',
|
||||
message: `Made folder ${folder.name} ${folder.public ? 'private' : 'public'}`,
|
||||
color: 'green',
|
||||
icon: <IconLockOpen size='1rem' />,
|
||||
icon: <UnlockIcon />,
|
||||
});
|
||||
folders.refetch();
|
||||
} else {
|
||||
@@ -128,7 +84,7 @@ export default function Folders({ disableMediaPreview, exifEnabled, compress })
|
||||
title: 'Failed to make folder public/private',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <IconLockCancel size='1rem' />,
|
||||
icon: <UnlockIcon />,
|
||||
});
|
||||
folders.refetch();
|
||||
}
|
||||
@@ -148,263 +104,102 @@ export default function Folders({ disableMediaPreview, exifEnabled, compress })
|
||||
folderId={activeFolderId}
|
||||
disableMediaPreview={disableMediaPreview}
|
||||
exifEnabled={exifEnabled}
|
||||
compress={compress}
|
||||
/>
|
||||
|
||||
<Group mb='md'>
|
||||
<Title>Folders</Title>
|
||||
<ActionIcon onClick={() => setCreateOpen(!createOpen)} component='a' variant='filled' color='primary'>
|
||||
<IconFolderPlus size='1rem' />
|
||||
<PlusIcon />
|
||||
</ActionIcon>
|
||||
<Tooltip label={listView ? 'Switch to grid view' : 'Switch to list view'}>
|
||||
<ActionIcon variant='filled' color='primary' onClick={() => setListView(!listView)}>
|
||||
{listView ? <IconList size='1rem' /> : <IconGridDots size='1rem' />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
{listView ? (
|
||||
<DataTable
|
||||
withBorder
|
||||
borderRadius='md'
|
||||
highlightOnHover
|
||||
verticalSpacing='sm'
|
||||
columns={[
|
||||
{ accessor: 'id', title: 'ID', sortable: true },
|
||||
{ accessor: 'name', sortable: true },
|
||||
|
||||
{
|
||||
accessor: 'public',
|
||||
sortable: true,
|
||||
render: (folder) => (folder.public ? 'Public' : 'Private'),
|
||||
},
|
||||
{
|
||||
accessor: 'createdAt',
|
||||
title: 'Created',
|
||||
sortable: true,
|
||||
render: (folder) => new Date(folder.createdAt).toLocaleString(),
|
||||
},
|
||||
{
|
||||
accessor: 'updatedAt',
|
||||
title: 'Last updated',
|
||||
sortable: true,
|
||||
render: (folder) => new Date(folder.updatedAt).toLocaleString(),
|
||||
},
|
||||
{
|
||||
accessor: 'actions',
|
||||
textAlignment: 'right',
|
||||
render: (folder) => (
|
||||
<Group spacing={4} position='right' noWrap>
|
||||
<Tooltip label='View files in folder'>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
setViewOpen(true);
|
||||
setActiveFolderId(folder.id);
|
||||
}}
|
||||
variant='subtle'
|
||||
color='primary'
|
||||
>
|
||||
<IconFiles size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label={folder.public ? 'Make folder private' : 'Make folder public'}>
|
||||
<ActionIcon onClick={() => makePublic(folder)} variant='subtle' color='primary'>
|
||||
{folder.public ? <IconLockOpen size='1rem' /> : <IconLock size='1rem' />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label='Open folder in new tab'>
|
||||
<ActionIcon
|
||||
onClick={() => window.open(`/folder/${folder.id}`, '_blank')}
|
||||
variant='subtle'
|
||||
color='primary'
|
||||
>
|
||||
<IconFolderShare size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label='Copy folder link'>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
clipboard.copy(`${window.location.origin}/folder/${folder.id}`);
|
||||
showNotification({
|
||||
title: 'Copied folder link',
|
||||
message: 'Copied folder link to clipboard',
|
||||
color: 'green',
|
||||
icon: <IconClipboardCheck size='1rem' />,
|
||||
});
|
||||
}}
|
||||
variant='subtle'
|
||||
color='primary'
|
||||
>
|
||||
<IconClipboardCopy size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label='Delete folder'>
|
||||
<ActionIcon onClick={() => deleteFolder(folder)} variant='subtle' color='red'>
|
||||
<IconFolderX size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
]}
|
||||
sortStatus={sortStatus}
|
||||
onSortStatusChange={setSortStatus}
|
||||
records={records ?? []}
|
||||
fetching={folders.isLoading}
|
||||
loaderBackgroundBlur={5}
|
||||
minHeight='calc(100vh - 200px)'
|
||||
loaderVariant='dots'
|
||||
rowContextMenu={{
|
||||
shadow: 'xl',
|
||||
borderRadius: 'md',
|
||||
items: (folder) => [
|
||||
{
|
||||
key: 'viewFiles',
|
||||
title: 'View files in folder',
|
||||
icon: <IconFiles size='1rem' />,
|
||||
onClick: () => {
|
||||
setViewOpen(true);
|
||||
setActiveFolderId(folder.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'makePublic',
|
||||
title: folder.public ? 'Make folder private' : 'Make folder public',
|
||||
icon: folder.public ? <IconLockOpen size='1rem' /> : <IconLock size='1rem' />,
|
||||
onClick: () => makePublic(folder),
|
||||
},
|
||||
{
|
||||
key: 'openFolder',
|
||||
title: 'Open folder in a new tab',
|
||||
icon: <IconExternalLink size='1rem' />,
|
||||
onClick: () => window.open(`/folder/${folder.id}`, '_blank'),
|
||||
},
|
||||
{
|
||||
key: 'copyLink',
|
||||
title: 'Copy folder link to clipboard',
|
||||
icon: <IconClipboardCopy size='1rem' />,
|
||||
onClick: () => {
|
||||
clipboard.copy(`${window.location.origin}/folder/${folder.id}`);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'deleteFolder',
|
||||
title: 'Delete folder',
|
||||
icon: <IconFolderX size='1rem' />,
|
||||
onClick: () => deleteFolder(folder),
|
||||
},
|
||||
],
|
||||
}}
|
||||
onCellClick={({ column, record: folder }) => {
|
||||
if (column.accessor === 'actions') return;
|
||||
|
||||
setViewOpen(true);
|
||||
setActiveFolderId(folder.id);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
|
||||
{folders.isSuccess
|
||||
? folders.data.length
|
||||
? folders.data.map((folder) => (
|
||||
<Card key={folder.id}>
|
||||
<Group position='apart'>
|
||||
<Group position='left'>
|
||||
<Avatar size='lg' color='primary'>
|
||||
{folder.id}
|
||||
</Avatar>
|
||||
<Stack spacing={0}>
|
||||
<Title>{folder.name}</Title>
|
||||
<MutedText size='sm'>ID: {folder.id}</MutedText>
|
||||
<MutedText size='sm'>Public: {folder.public ? 'Yes' : 'No'}</MutedText>
|
||||
<Tooltip label={new Date(folder.createdAt).toLocaleString()}>
|
||||
<div>
|
||||
<MutedText size='sm'>
|
||||
Created {relativeTime(new Date(folder.createdAt))}
|
||||
</MutedText>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip label={new Date(folder.updatedAt).toLocaleString()}>
|
||||
<div>
|
||||
<MutedText size='sm'>
|
||||
Last updated {relativeTime(new Date(folder.updatedAt))}
|
||||
</MutedText>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Group>
|
||||
<Stack>
|
||||
<Tooltip label={folder.public ? 'Make folder private' : 'Make folder public'}>
|
||||
<ActionIcon
|
||||
aria-label={folder.public ? 'make private' : 'make public'}
|
||||
onClick={() => makePublic(folder)}
|
||||
>
|
||||
{folder.public ? <IconLock size='1rem' /> : <IconLockOpen size='1rem' />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label='Delete folder'>
|
||||
<ActionIcon aria-label='delete' onClick={() => deleteFolder(folder)}>
|
||||
<IconFolderMinus size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
<Stack>
|
||||
<ActionIcon
|
||||
aria-label='view files'
|
||||
onClick={() => {
|
||||
setViewOpen(!viewOpen);
|
||||
setActiveFolderId(folder.id);
|
||||
}}
|
||||
>
|
||||
<IconFiles size='1rem' />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
aria-label='copy link'
|
||||
onClick={() => {
|
||||
clipboard.copy(`${window.location.origin}/folder/${folder.id}`);
|
||||
if (!navigator.clipboard)
|
||||
showNotification({
|
||||
title: 'Unable to copy to clipboard',
|
||||
message: 'Zipline is unable to copy to clipboard due to security reasons.',
|
||||
color: 'red',
|
||||
});
|
||||
else
|
||||
showNotification({
|
||||
title: 'Copied folder link',
|
||||
message: (
|
||||
<>
|
||||
Copied{' '}
|
||||
<AnchorNext href={`/folder/${folder.id}`}>folder link</AnchorNext> to
|
||||
clipboard
|
||||
</>
|
||||
),
|
||||
color: 'green',
|
||||
icon: <IconClipboardCopy size='1rem' />,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<IconClipboardCopy size='1rem' />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
aria-label='open in new tab'
|
||||
onClick={() => window.open(`/folder/${folder.id}`)}
|
||||
>
|
||||
<IconFolderShare size='1rem' />
|
||||
</ActionIcon>
|
||||
</Stack>
|
||||
</Group>
|
||||
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
|
||||
{folders.isSuccess
|
||||
? folders.data.length
|
||||
? folders.data.map((folder) => (
|
||||
<Card key={folder.id}>
|
||||
<Group position='apart'>
|
||||
<Group position='left'>
|
||||
<Avatar size='lg' color='primary'>
|
||||
{folder.id}
|
||||
</Avatar>
|
||||
<Stack spacing={0}>
|
||||
<Title>{folder.name}</Title>
|
||||
<MutedText size='sm'>ID: {folder.id}</MutedText>
|
||||
<MutedText size='sm'>Public: {folder.public ? 'Yes' : 'No'}</MutedText>
|
||||
<Tooltip label={new Date(folder.createdAt).toLocaleString()}>
|
||||
<div>
|
||||
<MutedText size='sm'>
|
||||
Created {relativeTime(new Date(folder.createdAt))}
|
||||
</MutedText>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip label={new Date(folder.updatedAt).toLocaleString()}>
|
||||
<div>
|
||||
<MutedText size='sm'>
|
||||
Last updated {relativeTime(new Date(folder.updatedAt))}
|
||||
</MutedText>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Card>
|
||||
))
|
||||
: null
|
||||
: [1, 2, 3, 4].map((x) => (
|
||||
<div key={x}>
|
||||
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }} />
|
||||
</div>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
<Group>
|
||||
<Tooltip label={folder.public ? 'Make folder private' : 'Make folder public'}>
|
||||
<ActionIcon
|
||||
aria-label={folder.public ? 'make private' : 'make public'}
|
||||
onClick={() => makePublic(folder)}
|
||||
>
|
||||
{folder.public ? <LockIcon /> : <UnlockIcon />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<ActionIcon
|
||||
aria-label='view files'
|
||||
onClick={() => {
|
||||
setViewOpen(!viewOpen);
|
||||
setActiveFolderId(folder.id);
|
||||
}}
|
||||
>
|
||||
<FileIcon />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
aria-label='copy link'
|
||||
onClick={() => {
|
||||
clipboard.copy(`${window.location.origin}/folder/${folder.id}`);
|
||||
if (!navigator.clipboard)
|
||||
showNotification({
|
||||
title: 'Unable to copy to clipboard',
|
||||
message: 'Zipline is unable to copy to clipboard due to security reasons.',
|
||||
color: 'red',
|
||||
});
|
||||
else
|
||||
showNotification({
|
||||
title: 'Copied folder link',
|
||||
message: (
|
||||
<>
|
||||
Copied <Link href={`/folder/${folder.id}`}>folder link</Link> to clipboard
|
||||
</>
|
||||
),
|
||||
color: 'green',
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<LinkIcon />
|
||||
</ActionIcon>
|
||||
<ActionIcon aria-label='delete' onClick={() => deleteFolder(folder)}>
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
))
|
||||
: null
|
||||
: [1, 2, 3, 4].map((x) => (
|
||||
<div key={x}>
|
||||
<Skeleton width='100%' height={220} sx={{ borderRadius: 1 }} />
|
||||
</div>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,24 +17,12 @@ import { useForm } from '@mantine/form';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import type { Invite } from '@prisma/client';
|
||||
import {
|
||||
IconClipboardCopy,
|
||||
IconGridDots,
|
||||
IconList,
|
||||
IconPlus,
|
||||
IconTag,
|
||||
IconTagOff,
|
||||
IconTrash,
|
||||
} from '@tabler/icons-react';
|
||||
import { CopyIcon, CrossIcon, DeleteIcon, PlusIcon, TagIcon } from 'components/icons';
|
||||
import MutedText from 'components/MutedText';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { listViewInvitesSelector } from 'lib/recoil/settings';
|
||||
import { expireText, relativeTime } from 'lib/utils/client';
|
||||
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
const expires = ['30m', '1h', '6h', '12h', '1d', '3d', '5d', '7d', 'never'];
|
||||
|
||||
@@ -77,14 +65,14 @@ function CreateInviteModal({ open, setOpen, updateInvites }) {
|
||||
showNotification({
|
||||
title: 'Failed to create invite',
|
||||
message: res.error,
|
||||
icon: <IconTagOff size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
color: 'red',
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Created invite',
|
||||
message: '',
|
||||
icon: <IconTag size='1rem' />,
|
||||
icon: <TagIcon />,
|
||||
color: 'green',
|
||||
});
|
||||
}
|
||||
@@ -137,56 +125,29 @@ export default function Invites() {
|
||||
const modals = useModals();
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const [invites, setInvites] = useState<Invite[]>([]);
|
||||
const [invites, setInvites] = useState([]);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [ok, setOk] = useState(false);
|
||||
|
||||
const [listView, setListView] = useRecoilState(listViewInvitesSelector);
|
||||
|
||||
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
|
||||
columnAccessor: 'createdAt',
|
||||
direction: 'asc',
|
||||
});
|
||||
const [records, setRecords] = useState(invites);
|
||||
|
||||
useEffect(() => {
|
||||
setRecords(invites);
|
||||
}, [invites]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!records || records.length === 0) return;
|
||||
|
||||
const sortedRecords = [...records].sort((a, b) => {
|
||||
if (sortStatus.direction === 'asc') {
|
||||
return a[sortStatus.columnAccessor] > b[sortStatus.columnAccessor] ? 1 : -1;
|
||||
}
|
||||
|
||||
return a[sortStatus.columnAccessor] < b[sortStatus.columnAccessor] ? 1 : -1;
|
||||
});
|
||||
|
||||
setRecords(sortedRecords);
|
||||
}, [sortStatus]);
|
||||
|
||||
const openDeleteModal = (invite) =>
|
||||
modals.openConfirmModal({
|
||||
title: `Delete ${invite.code}?`,
|
||||
centered: true,
|
||||
overlayProps: { blur: 3 },
|
||||
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}`,
|
||||
title: 'Failed to delete invite ${invite.code}',
|
||||
message: res.error,
|
||||
icon: <IconTagOff size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
color: 'red',
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: `Deleted invite ${invite.code}`,
|
||||
message: '',
|
||||
icon: <IconTag size='1rem' />,
|
||||
icon: <DeleteIcon />,
|
||||
color: 'green',
|
||||
});
|
||||
}
|
||||
@@ -207,7 +168,7 @@ export default function Invites() {
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: '',
|
||||
icon: <IconClipboardCopy size='1rem' />,
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -215,7 +176,6 @@ export default function Invites() {
|
||||
const us = await useFetch('/api/auth/invite');
|
||||
if (!us.error) {
|
||||
setInvites(us);
|
||||
setOk(true);
|
||||
} else {
|
||||
router.push('/dashboard');
|
||||
}
|
||||
@@ -231,134 +191,48 @@ export default function Invites() {
|
||||
<Group mb='md'>
|
||||
<Title>Invites</Title>
|
||||
<ActionIcon variant='filled' color='primary' onClick={() => setOpen(true)}>
|
||||
<IconPlus size='1rem' />
|
||||
<PlusIcon />
|
||||
</ActionIcon>
|
||||
<Tooltip label={listView ? 'Switch to grid view' : 'Switch to list view'}>
|
||||
<ActionIcon variant='filled' color='primary' onClick={() => setListView(!listView)}>
|
||||
{listView ? <IconList size='1rem' /> : <IconGridDots size='1rem' />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
{listView ? (
|
||||
<DataTable
|
||||
withBorder
|
||||
borderRadius='md'
|
||||
highlightOnHover
|
||||
verticalSpacing='sm'
|
||||
columns={[
|
||||
{ accessor: 'id', sortable: true },
|
||||
{ accessor: 'code', sortable: true },
|
||||
{
|
||||
accessor: 'createdAt',
|
||||
title: 'Created At',
|
||||
sortable: true,
|
||||
render: (invite) => new Date(invite.createdAt).toLocaleString(),
|
||||
},
|
||||
{
|
||||
accessor: 'expiresAt',
|
||||
title: 'Expires At',
|
||||
sortable: true,
|
||||
render: (invite) => new Date(invite.expiresAt).toLocaleString(),
|
||||
},
|
||||
{
|
||||
accessor: 'used',
|
||||
sortable: true,
|
||||
render: (invite) => (invite.used ? 'Yes' : 'No'),
|
||||
},
|
||||
{
|
||||
accessor: 'actions',
|
||||
textAlignment: 'right',
|
||||
render: (invite) => (
|
||||
<Group spacing={4} position='right' noWrap>
|
||||
<Tooltip label='Copy invite link'>
|
||||
<ActionIcon variant='subtle' color='primary' onClick={() => handleCopy(invite)}>
|
||||
<IconClipboardCopy size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label='Delete invite'>
|
||||
<ActionIcon variant='subtle' color='red' onClick={() => openDeleteModal(invite)}>
|
||||
<IconTrash size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
]}
|
||||
sortStatus={sortStatus}
|
||||
onSortStatusChange={setSortStatus}
|
||||
records={records ?? []}
|
||||
fetching={!ok}
|
||||
minHeight='calc(100vh - 200px)'
|
||||
loaderBackgroundBlur={5}
|
||||
loaderVariant='dots'
|
||||
rowContextMenu={{
|
||||
shadow: 'xl',
|
||||
borderRadius: 'md',
|
||||
items: (invite) => [
|
||||
{
|
||||
key: 'copy',
|
||||
icon: <IconClipboardCopy size='1rem' />,
|
||||
title: `Copy invite code: "${invite.code}"`,
|
||||
onClick: () => clipboard.copy(invite.code),
|
||||
},
|
||||
{
|
||||
key: 'copyLink',
|
||||
icon: <IconClipboardCopy size='1rem' />,
|
||||
title: 'Copy invite link',
|
||||
onClick: () => handleCopy(invite),
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
icon: <IconTrash size='1rem' />,
|
||||
title: `Delete invite ${invite.code}`,
|
||||
onClick: () => openDeleteModal(invite),
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<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>
|
||||
<Tooltip label={new Date(invite.createdAt).toLocaleString()}>
|
||||
<div>
|
||||
<MutedText size='sm'>
|
||||
Created {relativeTime(new Date(invite.createdAt))}
|
||||
</MutedText>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip label={new Date(invite.expiresAt).toLocaleString()}>
|
||||
<div>
|
||||
<MutedText size='sm'>{expireText(invite.expiresAt.toString())}</MutedText>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Stack>
|
||||
<ActionIcon aria-label='copy' onClick={() => handleCopy(invite)}>
|
||||
<IconClipboardCopy size='1rem' />
|
||||
</ActionIcon>
|
||||
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(invite)}>
|
||||
<IconTrash size='1rem' />
|
||||
</ActionIcon>
|
||||
<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>
|
||||
<Tooltip label={new Date(invite.createdAt).toLocaleString()}>
|
||||
<div>
|
||||
<MutedText size='sm'>Created {relativeTime(new Date(invite.createdAt))}</MutedText>
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip label={new Date(invite.expiresAt).toLocaleString()}>
|
||||
<div>
|
||||
<MutedText size='sm'>{expireText(invite.expiresAt)}</MutedText>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Card>
|
||||
))
|
||||
: [1, 2, 3].map((x) => <Skeleton key={x} width='100%' height={100} radius='sm' />)}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
<Stack>
|
||||
<ActionIcon aria-label='copy' onClick={() => handleCopy(invite)}>
|
||||
<CopyIcon />
|
||||
</ActionIcon>
|
||||
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(invite)}>
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</Stack>
|
||||
</Group>
|
||||
</Card>
|
||||
))
|
||||
: [1, 2, 3].map((x) => <Skeleton key={x} width='100%' height={100} radius='sm' />)}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, Checkbox, Group, Modal, Text, Title } from '@mantine/core';
|
||||
import { closeAllModals, openConfirmModal } from '@mantine/modals';
|
||||
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||
import { IconFiles, IconFilesOff } from '@tabler/icons-react';
|
||||
import { CheckIcon, CrossIcon } from 'components/icons';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
|
||||
export default function ClearStorage({ open, setOpen, check, setCheck }) {
|
||||
@@ -22,7 +22,7 @@ export default function ClearStorage({ open, setOpen, check, setCheck }) {
|
||||
title: 'Error while clearing uploads',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <IconFilesOff size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
} else {
|
||||
updateNotification({
|
||||
@@ -30,7 +30,7 @@ export default function ClearStorage({ open, setOpen, check, setCheck }) {
|
||||
title: 'Successfully cleared uploads',
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <IconFiles size='1rem' />,
|
||||
icon: <CheckIcon />,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Code } from '@mantine/core';
|
||||
import AnchorNext from 'components/AnchorNext';
|
||||
import Link from 'components/Link';
|
||||
import { GeneratorModal } from './GeneratorModal';
|
||||
|
||||
export default function Flameshot({ user, open, setOpen }) {
|
||||
@@ -105,18 +105,18 @@ ${curl.join(' ')} -d "{\\"url\\": \\"$arg\\"}"${values.noJSON ? '' : " | jq -r '
|
||||
title='Flameshot'
|
||||
desc={
|
||||
<>
|
||||
To use this script, you need <AnchorNext href='https://flameshot.org'>Flameshot</AnchorNext>,{' '}
|
||||
<AnchorNext href='https://curl.se/'>
|
||||
To use this script, you need <Link href='https://flameshot.org'>Flameshot</Link>,{' '}
|
||||
<Link href='https://curl.se/'>
|
||||
<Code>curl</Code>
|
||||
</AnchorNext>
|
||||
</Link>
|
||||
,{' '}
|
||||
<AnchorNext href='https://github.com/stedolan/jq'>
|
||||
<Link href='https://github.com/stedolan/jq'>
|
||||
<Code>jq</Code>
|
||||
</AnchorNext>
|
||||
</Link>
|
||||
, and{' '}
|
||||
<AnchorNext href='https://github.com/kfish/xsel'>
|
||||
<Link href='https://github.com/kfish/xsel'>
|
||||
<Code>xsel</Code>
|
||||
</AnchorNext>{' '}
|
||||
</Link>{' '}
|
||||
installed. This script is intended for use on Linux only.
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ import {
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { IconFileDownload, IconWorld } from '@tabler/icons-react';
|
||||
import AnchorNext from 'components/AnchorNext';
|
||||
import { DownloadIcon, GlobeIcon } from 'components/icons';
|
||||
import Link from 'components/Link';
|
||||
import MutedText from 'components/MutedText';
|
||||
import { useReducer, useState } from 'react';
|
||||
|
||||
@@ -103,11 +103,10 @@ export function GeneratorModal({ opened, onClose, title, onSubmit, ...other }) {
|
||||
<Select
|
||||
label='Select file name format'
|
||||
data={[
|
||||
{ value: 'random', label: 'Random (alphanumeric)' },
|
||||
{ value: 'date', label: 'Date' },
|
||||
{ value: 'uuid', label: 'UUID' },
|
||||
{ value: 'name', label: 'Name (keeps original file name)' },
|
||||
{ value: 'gfycat', label: 'Gfycat' },
|
||||
{ value: 'RANDOM', label: 'Random (alphanumeric)' },
|
||||
{ value: 'DATE', label: 'Date' },
|
||||
{ value: 'UUID', label: 'UUID' },
|
||||
{ value: 'NAME', label: 'Name (keeps original file name)' },
|
||||
]}
|
||||
id='format'
|
||||
my='sm'
|
||||
@@ -129,7 +128,7 @@ export function GeneratorModal({ opened, onClose, title, onSubmit, ...other }) {
|
||||
<TextInput
|
||||
label='Override Domain'
|
||||
onChange={handleOD}
|
||||
icon={<IconWorld size='1rem' />}
|
||||
icon={<GlobeIcon />}
|
||||
description={odState.description}
|
||||
error={odState.error}
|
||||
/>
|
||||
@@ -169,9 +168,9 @@ export function GeneratorModal({ opened, onClose, title, onSubmit, ...other }) {
|
||||
<Text>Wayland</Text>
|
||||
<MutedText size='sm'>
|
||||
If using wayland, you can check the boxes below to your liking. This will require{' '}
|
||||
<AnchorNext href='https://github.com/bugaevc/wl-clipboard'>
|
||||
<Link href='https://github.com/bugaevc/wl-clipboard'>
|
||||
<Code>wl-clipboard</Code>
|
||||
</AnchorNext>{' '}
|
||||
</Link>{' '}
|
||||
for the <Code>wl-copy</Code> command.
|
||||
</MutedText>
|
||||
</Box>
|
||||
@@ -197,8 +196,8 @@ export function GeneratorModal({ opened, onClose, title, onSubmit, ...other }) {
|
||||
description={
|
||||
<>
|
||||
If using a compositor such as{' '}
|
||||
<AnchorNext href='https://github.com/hyprwm/hyprland'>Hyprland</AnchorNext>, this option
|
||||
will set the <Code>XDG_CURRENT_DESKTOP=sway</Code> to workaround Flameshot's errors
|
||||
<Link href='https://github.com/hyprwm/hyprland'>Hyprland</Link>, this option will set the{' '}
|
||||
<Code>XDG_CURRENT_DESKTOP=sway</Code> to workaround Flameshot's errors
|
||||
</>
|
||||
}
|
||||
disabled={!isUploadFile}
|
||||
@@ -212,7 +211,7 @@ export function GeneratorModal({ opened, onClose, title, onSubmit, ...other }) {
|
||||
<Group grow my='md'>
|
||||
<Button onClick={form.reset}>Reset</Button>
|
||||
|
||||
<Button rightIcon={<IconFileDownload size='1rem' />} type='submit'>
|
||||
<Button rightIcon={<DownloadIcon />} type='submit'>
|
||||
Download
|
||||
</Button>
|
||||
</Group>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useReducer, useState } from 'react';
|
||||
import { GeneratorModal } from './GeneratorModal';
|
||||
|
||||
export default function ShareX({ user, open, setOpen }) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Button, Center, Image, Modal, PinInput, Text, Title } from '@mantine/core';
|
||||
import { Button, Center, Image, Modal, NumberInput, Text, Title } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { Icon2fa, IconBarcodeOff, IconCheck } from '@tabler/icons-react';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { CheckIcon, CrossIcon } from 'components/icons';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
@@ -8,7 +9,9 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
|
||||
const [secret, setSecret] = useState('');
|
||||
const [qrCode, setQrCode] = useState('');
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
const [code, setCode] = useState(undefined);
|
||||
const [error, setError] = useState('');
|
||||
const form = useForm();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -20,7 +23,7 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
|
||||
title: 'Error',
|
||||
message: "Can't generate code as you are already using MFA",
|
||||
color: 'red',
|
||||
icon: <IconBarcodeOff size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
} else {
|
||||
setSecret(data.secret);
|
||||
@@ -31,15 +34,15 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
|
||||
})();
|
||||
}, [opened]);
|
||||
|
||||
const disableTotp = async (code) => {
|
||||
const disableTotp = async () => {
|
||||
setDisabled(true);
|
||||
if (code.length !== 6) {
|
||||
setDisabled(false);
|
||||
const str = code.toString();
|
||||
if (str.length !== 6) {
|
||||
return setError('Code must be 6 digits');
|
||||
}
|
||||
|
||||
const resp = await useFetch('/api/user/mfa/totp', 'DELETE', {
|
||||
code,
|
||||
code: str,
|
||||
});
|
||||
|
||||
if (resp.error) {
|
||||
@@ -47,9 +50,9 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Success',
|
||||
message: 'Successfully disabled 2FA',
|
||||
message: 'Successfully disabled MFA',
|
||||
color: 'green',
|
||||
icon: <Icon2fa size='1rem' />,
|
||||
icon: <CheckIcon />,
|
||||
});
|
||||
|
||||
setTotpEnabled(false);
|
||||
@@ -60,16 +63,16 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
|
||||
setDisabled(false);
|
||||
};
|
||||
|
||||
const verifyCode = async (code) => {
|
||||
const verifyCode = async () => {
|
||||
setDisabled(true);
|
||||
if (code.length !== 6) {
|
||||
setDisabled(false);
|
||||
const str = code.toString();
|
||||
if (str.length !== 6) {
|
||||
return setError('Code must be 6 digits');
|
||||
}
|
||||
|
||||
const resp = await useFetch('/api/user/mfa/totp', 'POST', {
|
||||
secret,
|
||||
code,
|
||||
code: str,
|
||||
register: true,
|
||||
});
|
||||
|
||||
@@ -78,9 +81,9 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Success',
|
||||
message: 'Successfully enabled 2FA',
|
||||
message: 'Successfully enabled MFA',
|
||||
color: 'green',
|
||||
icon: <Icon2fa size='1rem' />,
|
||||
icon: <CheckIcon />,
|
||||
});
|
||||
|
||||
setTotpEnabled(true);
|
||||
@@ -91,13 +94,6 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
|
||||
setDisabled(false);
|
||||
};
|
||||
|
||||
const handlePinChange = (value) => {
|
||||
if (value.length === 6) {
|
||||
setDisabled(true);
|
||||
deleteTotp ? disableTotp(value) : verifyCode(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={opened}
|
||||
@@ -116,46 +112,39 @@ export function TotpModal({ opened, onClose, deleteTotp, setTotpEnabled }) {
|
||||
<Center>
|
||||
<Image height={180} width={180} src={qrCode} alt='QR Code' withPlaceholder />
|
||||
</Center>
|
||||
<Text my='sm'>QR Code not working? Try manually entering the code into your app: {secret}</Text>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Center my='md'>
|
||||
<PinInput
|
||||
data-autofocus
|
||||
length={6}
|
||||
oneTimeCode
|
||||
type='number'
|
||||
placeholder=''
|
||||
onChange={handlePinChange}
|
||||
autoFocus={true}
|
||||
error={!!error}
|
||||
disabled={disabled}
|
||||
size='xl'
|
||||
/>
|
||||
</Center>
|
||||
|
||||
{error && (
|
||||
<Text my='sm' size='sm' color='red' align='center'>
|
||||
{error}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!deleteTotp && (
|
||||
<Text my='sm' size='sm' color='gray' align='center'>
|
||||
QR Code not working? Try manually entering the code into your app: {secret}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Button
|
||||
disabled={disabled}
|
||||
size='lg'
|
||||
fullWidth
|
||||
mt='md'
|
||||
rightIcon={<IconCheck size='1rem' />}
|
||||
type='submit'
|
||||
<form
|
||||
onSubmit={form.onSubmit(() => {
|
||||
deleteTotp ? disableTotp() : verifyCode();
|
||||
})}
|
||||
>
|
||||
Verify{deleteTotp ? ' and Disable' : ''}
|
||||
</Button>
|
||||
<NumberInput
|
||||
placeholder='2FA Code'
|
||||
label='Verify'
|
||||
size='xl'
|
||||
hideControls
|
||||
maxLength={6}
|
||||
minLength={6}
|
||||
value={code}
|
||||
onChange={(e) => setCode(e)}
|
||||
data-autofocus
|
||||
error={error}
|
||||
/>
|
||||
|
||||
<Button
|
||||
disabled={disabled}
|
||||
size='lg'
|
||||
fullWidth
|
||||
mt='md'
|
||||
rightIcon={<CheckIcon />}
|
||||
onClick={deleteTotp ? disableTotp : verifyCode}
|
||||
>
|
||||
Verify{deleteTotp ? ' and Disable' : ''}
|
||||
</Button>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,33 +20,26 @@ import { randomId, useInterval, useMediaQuery } from '@mantine/hooks';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconBrandDiscordFilled,
|
||||
IconBrandGithubFilled,
|
||||
IconBrandGoogle,
|
||||
IconFileExport,
|
||||
IconFiles,
|
||||
IconFilesOff,
|
||||
IconFileZip,
|
||||
IconGraph,
|
||||
IconGraphOff,
|
||||
IconPhotoMinus,
|
||||
IconReload,
|
||||
IconTrash,
|
||||
IconUserCheck,
|
||||
IconUserCog,
|
||||
IconUserExclamation,
|
||||
IconUserMinus,
|
||||
IconUserX,
|
||||
} from '@tabler/icons-react';
|
||||
import AnchorNext from 'components/AnchorNext';
|
||||
import { FlameshotIcon, ShareXIcon } from 'components/icons';
|
||||
CheckIcon,
|
||||
CrossIcon,
|
||||
DeleteIcon,
|
||||
DiscordIcon,
|
||||
FlameshotIcon,
|
||||
GitHubIcon,
|
||||
GoogleIcon,
|
||||
RefreshIcon,
|
||||
SettingsIcon,
|
||||
ShareXIcon,
|
||||
} from 'components/icons';
|
||||
import DownloadIcon from 'components/icons/DownloadIcon';
|
||||
import TrashIcon from 'components/icons/TrashIcon';
|
||||
import Link from 'components/Link';
|
||||
import MutedText from 'components/MutedText';
|
||||
import { SmallTable } from 'components/SmallTable';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
import { bytesToHuman } from 'lib/utils/bytes';
|
||||
import { capitalize } from 'lib/utils/client';
|
||||
import Link from 'next/link';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import ClearStorage from './ClearStorage';
|
||||
@@ -69,9 +62,9 @@ function ExportDataTooltip({ children }) {
|
||||
export default function Manage({ oauth_registration, oauth_providers: raw_oauth_providers, totp_enabled }) {
|
||||
const oauth_providers = JSON.parse(raw_oauth_providers);
|
||||
const icons = {
|
||||
Discord: IconBrandDiscordFilled,
|
||||
GitHub: IconBrandGithubFilled,
|
||||
Google: IconBrandGoogle,
|
||||
Discord: DiscordIcon,
|
||||
GitHub: GitHubIcon,
|
||||
Google: GoogleIcon,
|
||||
};
|
||||
|
||||
for (const provider of oauth_providers) {
|
||||
@@ -86,7 +79,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
const [flameshotOpen, setFlameshotOpen] = useState(false);
|
||||
const [clrStorOpen, setClrStorOpen] = useState(false);
|
||||
const [exports, setExports] = useState([]);
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [file, setFile] = useState<File>(null);
|
||||
const [fileDataURL, setFileDataURL] = useState(user.avatar ?? null);
|
||||
const [totpEnabled, setTotpEnabled] = useState(!!user.totpSecret);
|
||||
const [checked, setCheck] = useState(false);
|
||||
@@ -110,13 +103,11 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
const handleAvatarChange = async (file: File) => {
|
||||
setFile(file);
|
||||
|
||||
if (file) setFileDataURL(await getDataURL(file));
|
||||
setFileDataURL(await getDataURL(file));
|
||||
};
|
||||
|
||||
const saveAvatar = async () => {
|
||||
let dataURL = null;
|
||||
|
||||
if (file) dataURL = await getDataURL(file);
|
||||
const dataURL = await getDataURL(file);
|
||||
|
||||
showNotification({
|
||||
id: 'update-user',
|
||||
@@ -128,7 +119,6 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
|
||||
const newUser = await useFetch('/api/user', 'PATCH', {
|
||||
avatar: dataURL,
|
||||
...(!dataURL && { resetAvatar: true }),
|
||||
});
|
||||
|
||||
if (newUser.error) {
|
||||
@@ -137,7 +127,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
title: "Couldn't save user",
|
||||
message: newUser.error,
|
||||
color: 'red',
|
||||
icon: <IconUserX size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
} else {
|
||||
setUser(newUser);
|
||||
@@ -145,8 +135,6 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
id: 'update-user',
|
||||
title: 'Saved User',
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <IconUserCheck size='1rem' />,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -213,7 +201,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
</>
|
||||
),
|
||||
color: 'red',
|
||||
icon: <IconUserX size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
updateNotification({
|
||||
@@ -221,7 +209,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
title: "Couldn't save user",
|
||||
message: newUser.error,
|
||||
color: 'red',
|
||||
icon: <IconUserX size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
} else {
|
||||
setUser(newUser);
|
||||
@@ -229,8 +217,6 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
id: 'update-user',
|
||||
title: 'Saved User',
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <IconUserCheck size='1rem' />,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -249,7 +235,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
title: 'Error exporting data',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <IconFileExport size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -278,14 +264,14 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
title: "Couldn't delete files",
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <IconFilesOff size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Deleted files',
|
||||
message: `${res.count} files deleted`,
|
||||
color: 'green',
|
||||
icon: <IconFiles size='1rem' />,
|
||||
icon: <DeleteIcon />,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -317,14 +303,14 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
title: 'Error updating stats',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <IconGraphOff size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Updated stats',
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <IconGraph size='1rem' />,
|
||||
icon: <CheckIcon />,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -338,7 +324,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
title: 'Error while unlinking from OAuth',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <IconUserExclamation size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
} else {
|
||||
setUser(res);
|
||||
@@ -346,7 +332,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
title: `Unlinked from ${provider[0] + provider.slice(1).toLowerCase()}`,
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <IconUserMinus size='1rem' />,
|
||||
icon: <CheckIcon />,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -355,15 +341,14 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
useEffect(() => {
|
||||
getExports();
|
||||
interval.start();
|
||||
}, [totpEnabled]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title>Manage User</Title>
|
||||
<MutedText size='md'>
|
||||
Want to use variables in embed text? Visit{' '}
|
||||
<AnchorNext href='https://zipline.diced.tech/docs/guides/variables'>the docs</AnchorNext> for
|
||||
variables
|
||||
<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' my='sm' {...form.getInputProps('username')} />
|
||||
@@ -428,7 +413,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
<Box my='md'>
|
||||
<Title>Two Factor Authentication</Title>
|
||||
<MutedText size='md'>
|
||||
{totpEnabled
|
||||
{user.totpSecret
|
||||
? 'You have two factor authentication enabled.'
|
||||
: 'You do not have two factor authentication enabled.'}
|
||||
</MutedText>
|
||||
@@ -467,9 +452,11 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
!user.oauth?.map(({ provider }) => provider.toLowerCase()).includes(x.name.toLowerCase())
|
||||
)
|
||||
.map(({ link_url, name, Icon }, i) => (
|
||||
<Button key={i} size='lg' leftIcon={<Icon />} component={Link} href={link_url} my='sm'>
|
||||
Link account with {name}
|
||||
</Button>
|
||||
<Link key={i} href={link_url} passHref legacyBehavior>
|
||||
<Button size='lg' leftIcon={<Icon colorScheme='manage' />} component='a' my='sm'>
|
||||
Link account with {name}
|
||||
</Button>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{user?.oauth?.map(({ provider }, i) => (
|
||||
@@ -477,7 +464,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
key={i}
|
||||
onClick={() => handleOauthUnlink(provider)}
|
||||
size='lg'
|
||||
leftIcon={<IconTrash size='1rem' />}
|
||||
leftIcon={<TrashIcon />}
|
||||
my='sm'
|
||||
color='red'
|
||||
>
|
||||
@@ -501,18 +488,16 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
<Card mt='md'>
|
||||
<Text>Preview:</Text>
|
||||
<Button
|
||||
leftIcon={
|
||||
fileDataURL ? (
|
||||
<Image src={fileDataURL} height={32} width={32} radius='md' />
|
||||
) : (
|
||||
<IconUserCog size='1rem' />
|
||||
)
|
||||
}
|
||||
leftIcon={fileDataURL ? <Image src={fileDataURL} height={32} radius='md' /> : <SettingsIcon />}
|
||||
sx={(t) => ({
|
||||
backgroundColor: '#00000000',
|
||||
'&:hover': {
|
||||
backgroundColor: t.other.hover,
|
||||
},
|
||||
color: t.colorScheme === 'dark' ? 'white' : 'black',
|
||||
})}
|
||||
size='xl'
|
||||
p='sm'
|
||||
variant='subtle'
|
||||
color='gray'
|
||||
compact
|
||||
>
|
||||
{user.username}
|
||||
</Button>
|
||||
@@ -538,15 +523,15 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
</Box>
|
||||
|
||||
<Group my='md' grow={useMediaQuery('(max-width: 768px)')}>
|
||||
<Button onClick={openDeleteModal} rightIcon={<IconPhotoMinus size='1rem' />} color='red'>
|
||||
<Button onClick={openDeleteModal} rightIcon={<DeleteIcon />} color='red'>
|
||||
Delete All Data
|
||||
</Button>
|
||||
<ExportDataTooltip>
|
||||
<Button onClick={exportData} rightIcon={<IconFileZip size='1rem' />}>
|
||||
<Button onClick={exportData} rightIcon={<DownloadIcon />}>
|
||||
Export Data
|
||||
</Button>
|
||||
</ExportDataTooltip>
|
||||
<Button onClick={getExports} rightIcon={<IconReload size='1rem' />}>
|
||||
<Button onClick={getExports} rightIcon={<RefreshIcon />}>
|
||||
Refresh
|
||||
</Button>
|
||||
</Group>
|
||||
@@ -581,15 +566,10 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
<Box mt='md'>
|
||||
<Title>Server</Title>
|
||||
<Group my='md' grow={useMediaQuery('(max-width: 768px)')}>
|
||||
<Button size='md' onClick={forceUpdateStats} color='red' rightIcon={<IconReload size='1rem' />}>
|
||||
<Button size='md' onClick={forceUpdateStats} color='red' rightIcon={<RefreshIcon />}>
|
||||
Force Update Stats
|
||||
</Button>
|
||||
<Button
|
||||
size='md'
|
||||
onClick={() => setClrStorOpen(true)}
|
||||
color='red'
|
||||
rightIcon={<IconTrash size='1rem' />}
|
||||
>
|
||||
<Button size='md' onClick={() => setClrStorOpen(true)} color='red' rightIcon={<TrashIcon />}>
|
||||
Delete all uploads
|
||||
</Button>
|
||||
</Group>
|
||||
@@ -601,7 +581,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
<Button
|
||||
size='xl'
|
||||
onClick={() => setShareXOpen(true)}
|
||||
rightIcon={<ShareXIcon size='1rem' />}
|
||||
rightIcon={<ShareXIcon />}
|
||||
sx={{
|
||||
'@media screen and (max-width: 768px)': {
|
||||
width: '100%',
|
||||
@@ -613,7 +593,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
|
||||
<Button
|
||||
size='xl'
|
||||
onClick={() => setFlameshotOpen(true)}
|
||||
rightIcon={<FlameshotIcon size='1rem' />}
|
||||
rightIcon={<FlameshotIcon />}
|
||||
sx={{
|
||||
'@media screen and (max-width: 768px)': {
|
||||
width: '100%',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, Center, Group, Skeleton, Table, TextInput, Title } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconClipboardCopy } from '@tabler/icons-react';
|
||||
import { CopyIcon } from 'components/icons';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function MetadataView({ fileId }) {
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: value,
|
||||
icon: <IconClipboardCopy size='1rem' />,
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Box, Card, Grid, LoadingOverlay, Title, useMantineTheme } from '@mantin
|
||||
|
||||
import { useStats } from 'lib/queries/stats';
|
||||
import { bytesToHuman } from 'lib/utils/bytes';
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Box, Card, Center, Grid, LoadingOverlay, Title, useMantineTheme } from
|
||||
import { SmallTable } from 'components/SmallTable';
|
||||
import { useStats } from 'lib/queries/stats';
|
||||
import { colorHash } from 'lib/utils/client';
|
||||
import { useMemo } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts';
|
||||
|
||||
@@ -34,11 +34,9 @@ export default function Types() {
|
||||
return !latest ? (
|
||||
<LoadingOverlay visible={stats.isLoading} />
|
||||
) : (
|
||||
<Box my='md'>
|
||||
<Box mt='md'>
|
||||
{latest.data.count_by_user.length ? (
|
||||
<Card my='md'>
|
||||
<Title size='h4'>Top Uploaders</Title>
|
||||
|
||||
<Card>
|
||||
<SmallTable
|
||||
columns={[
|
||||
{ id: 'username', name: 'Name' },
|
||||
@@ -48,7 +46,7 @@ export default function Types() {
|
||||
/>
|
||||
</Card>
|
||||
) : null}
|
||||
<Card my='md'>
|
||||
<Card>
|
||||
<Title size='h4'>Upload Types</Title>
|
||||
<Grid>
|
||||
<Grid.Col md={12} lg={8}>
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import { Button, Collapse, Group, Progress, Stack, Title } from '@mantine/core';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Collapse,
|
||||
Group,
|
||||
NumberInput,
|
||||
PasswordInput,
|
||||
Progress,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { randomId, useClipboard } from '@mantine/hooks';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||
import { IconFileImport, IconFileTime, IconFileUpload, IconFileX } from '@tabler/icons-react';
|
||||
import Dropzone from 'components/dropzone/Dropzone';
|
||||
import FileDropzone from 'components/dropzone/DropzoneFile';
|
||||
import { ClockIcon, CrossIcon, UploadIcon } from 'components/icons';
|
||||
import MutedText from 'components/MutedText';
|
||||
import { invalidateFiles } from 'lib/queries/files';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
@@ -26,23 +39,16 @@ export default function File({ chunks: chunks_config }) {
|
||||
const [options, setOpened, OptionsModal] = useUploadOptions();
|
||||
|
||||
useEffect(() => {
|
||||
const listener = (e: ClipboardEvent) => {
|
||||
window.addEventListener('paste', (e: ClipboardEvent) => {
|
||||
const item = Array.from(e.clipboardData.items).find((x) => /^image/.test(x.type));
|
||||
if (!item) return;
|
||||
|
||||
const file = item.getAsFile();
|
||||
|
||||
setFiles([...files, file]);
|
||||
showNotification({
|
||||
title: 'Image imported from clipboard',
|
||||
message: '',
|
||||
icon: <IconFileImport size='1rem' />,
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('paste', listener);
|
||||
return () => document.removeEventListener('paste', listener);
|
||||
}, []);
|
||||
});
|
||||
});
|
||||
|
||||
const handleChunkedFiles = async (expiresAt: Date, toChunkFiles: File[]) => {
|
||||
for (let i = 0; i !== toChunkFiles.length; ++i) {
|
||||
@@ -77,7 +83,7 @@ export default function File({ chunks: chunks_config }) {
|
||||
id: 'upload-chunked',
|
||||
title: 'Finalizing partial upload',
|
||||
message: 'This may take a while...',
|
||||
icon: <IconFileTime size='1rem' />,
|
||||
icon: <ClockIcon />,
|
||||
color: 'yellow',
|
||||
autoClose: false,
|
||||
});
|
||||
@@ -102,7 +108,7 @@ export default function File({ chunks: chunks_config }) {
|
||||
title: `Uploading chunk ${j + 1}/${chunks.length} Successful`,
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <IconFileUpload size='1rem' />,
|
||||
icon: <UploadIcon />,
|
||||
autoClose: false,
|
||||
});
|
||||
|
||||
@@ -112,7 +118,7 @@ export default function File({ chunks: chunks_config }) {
|
||||
title: 'Upload Successful',
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <IconFileUpload size='1rem' />,
|
||||
icon: <UploadIcon />,
|
||||
});
|
||||
showFilesModal(clipboard, modals, json.files);
|
||||
invalidateFiles();
|
||||
@@ -137,7 +143,7 @@ export default function File({ chunks: chunks_config }) {
|
||||
title: `Uploading chunk ${j + 1}/${chunks.length} Failed`,
|
||||
message: json.error,
|
||||
color: 'red',
|
||||
icon: <IconFileX size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
autoClose: false,
|
||||
});
|
||||
ready = false;
|
||||
@@ -231,7 +237,7 @@ export default function File({ chunks: chunks_config }) {
|
||||
title: 'Upload Successful',
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <IconFileUpload size='1rem' />,
|
||||
icon: <UploadIcon />,
|
||||
});
|
||||
showFilesModal(clipboard, modals, json.files);
|
||||
setFiles([]);
|
||||
@@ -254,7 +260,7 @@ export default function File({ chunks: chunks_config }) {
|
||||
title: 'Upload Failed',
|
||||
message: json.error,
|
||||
color: 'red',
|
||||
icon: <IconFileX size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
}
|
||||
setProgress(0);
|
||||
@@ -314,7 +320,7 @@ export default function File({ chunks: chunks_config }) {
|
||||
Clear Files
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={<IconFileUpload size='1rem' />}
|
||||
leftIcon={<UploadIcon />}
|
||||
onClick={handleUpload}
|
||||
disabled={files.length === 0 ? true : false}
|
||||
>
|
||||
|
||||
@@ -2,8 +2,8 @@ import { Alert, Button, Card, Container, Group, Select, Tabs, Title } from '@man
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||
import { IconCursorText, IconFileInfinity, IconFileUpload, IconPhoto } from '@tabler/icons-react';
|
||||
import CodeInput from 'components/CodeInput';
|
||||
import { ImageIcon, TypeIcon, UploadIcon } from 'components/icons';
|
||||
import KaTeX from 'components/render/KaTeX';
|
||||
import Markdown from 'components/render/Markdown';
|
||||
import PrismCode from 'components/render/PrismCode';
|
||||
@@ -83,10 +83,10 @@ export default function Text() {
|
||||
|
||||
<Tabs defaultValue='text' variant='pills'>
|
||||
<Tabs.List>
|
||||
<Tabs.Tab value='text' icon={<IconCursorText size='1rem' />}>
|
||||
<Tabs.Tab value='text' icon={<TypeIcon />}>
|
||||
Text
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value='preview' icon={<IconPhoto size='1rem' />}>
|
||||
<Tabs.Tab value='preview' icon={<ImageIcon />}>
|
||||
Preview
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
@@ -125,7 +125,7 @@ export default function Text() {
|
||||
onChange={setLang}
|
||||
dropdownPosition='top'
|
||||
data={Object.keys(exts).map((x) => ({ value: x, label: exts[x] }))}
|
||||
icon={<IconFileInfinity size='1rem' />}
|
||||
icon={<TypeIcon />}
|
||||
searchable
|
||||
/>
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function Text() {
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
leftIcon={<IconFileUpload size='1rem' />}
|
||||
leftIcon={<UploadIcon />}
|
||||
onClick={handleUpload}
|
||||
disabled={value.trim().length === 0 ? true : false}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ActionIcon, Group, Stack, Table, Title, Tooltip } from '@mantine/core';
|
||||
import { ActionIcon, Box, Button, Group, Stack, Table, Title, Tooltip } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconClipboardCopy, IconExternalLink } from '@tabler/icons-react';
|
||||
import AnchorNext from 'components/AnchorNext';
|
||||
import { CopyIcon, LinkIcon } from 'components/icons';
|
||||
import Link from 'components/Link';
|
||||
|
||||
export default function showFilesModal(clipboard, modals, files: string[]) {
|
||||
const open = (idx: number) => window.open(files[idx], '_blank');
|
||||
@@ -16,8 +16,8 @@ export default function showFilesModal(clipboard, modals, files: string[]) {
|
||||
else
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: <AnchorNext href={files[idx]}>{files[idx]}</AnchorNext>,
|
||||
icon: <IconClipboardCopy size='1rem' />,
|
||||
message: <Link href={files[idx]}>{files[idx]}</Link>,
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -30,17 +30,17 @@ export default function showFilesModal(clipboard, modals, files: string[]) {
|
||||
{files.map((file, idx) => (
|
||||
<Group key={idx} position='apart'>
|
||||
<Group position='left'>
|
||||
<AnchorNext href={file}>{file}</AnchorNext>
|
||||
<Link href={file}>{file}</Link>
|
||||
</Group>
|
||||
<Group position='right'>
|
||||
<Tooltip label='Open link in a new tab'>
|
||||
<ActionIcon onClick={() => open(idx)} variant='filled' color='primary'>
|
||||
<IconExternalLink size='1rem' />
|
||||
<LinkIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label='Copy link to clipboard'>
|
||||
<ActionIcon onClick={() => copy(idx)} variant='filled' color='primary'>
|
||||
<IconClipboardCopy size='1rem' />
|
||||
<CopyIcon />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconAlarm, IconEye, IconFileInfo, IconKey, IconPhotoDown, IconWorld } from '@tabler/icons-react';
|
||||
import React, { Dispatch, ReactNode, SetStateAction, useReducer, useState } from 'react';
|
||||
import { ClockIcon, ImageIcon, KeyIcon, TypeIcon, UserIcon, GlobeIcon } from 'components/icons';
|
||||
import React, { Dispatch, SetStateAction, useReducer, useState } from 'react';
|
||||
|
||||
export type UploadOptionsState = {
|
||||
expires: string;
|
||||
@@ -37,11 +37,7 @@ export function OptionsModal({
|
||||
opened: boolean;
|
||||
setOpened: Dispatch<SetStateAction<boolean>>;
|
||||
state: UploadOptionsState;
|
||||
setState: Dispatch<
|
||||
SetStateAction<{
|
||||
[key in keyof UploadOptionsState]?: UploadOptionsState[key];
|
||||
}>
|
||||
>;
|
||||
setState: Dispatch<SetStateAction<any>>;
|
||||
reset: () => void;
|
||||
}) {
|
||||
const [odState, setODState] = useReducer((state, newState) => ({ ...state, ...newState }), {
|
||||
@@ -83,16 +79,16 @@ export function OptionsModal({
|
||||
label='Max Views'
|
||||
description='The maximum number of times this file can be viewed. Leave blank for unlimited views.'
|
||||
value={state.maxViews}
|
||||
onChange={(e) => setState({ maxViews: e === '' ? undefined : e })}
|
||||
onChange={(e) => setState({ maxViews: e })}
|
||||
min={0}
|
||||
icon={<IconEye size='1rem' />}
|
||||
icon={<UserIcon />}
|
||||
/>
|
||||
<Select
|
||||
label='Expires'
|
||||
description='The date and time this file will expire. Leave blank for never.'
|
||||
value={state.expires}
|
||||
onChange={(e) => setState({ expires: e })}
|
||||
icon={<IconAlarm size='1rem' />}
|
||||
icon={<ClockIcon size={14} />}
|
||||
data={[
|
||||
{ value: 'never', label: 'Never' },
|
||||
{ value: '5min', label: '5 minutes' },
|
||||
@@ -122,11 +118,6 @@ export function OptionsModal({
|
||||
{ value: '6m', label: '6 months' },
|
||||
{ value: '8m', label: '8 months' },
|
||||
{ value: '1y', label: '1 year' },
|
||||
{
|
||||
value: null,
|
||||
label: 'Need more freedom? Set an exact date and time through the API.',
|
||||
disabled: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
@@ -134,18 +125,12 @@ export function OptionsModal({
|
||||
description='The compression level to use when uploading this file. Leave blank for default.'
|
||||
value={state.compression}
|
||||
onChange={(e) => setState({ compression: e })}
|
||||
icon={<IconPhotoDown size='1rem' />}
|
||||
icon={<ImageIcon />}
|
||||
data={[
|
||||
{ value: 'none', label: 'None' },
|
||||
{ value: '25', label: 'Low (25%)' },
|
||||
{ value: '50', label: 'Medium (50%)' },
|
||||
{ value: '75', label: 'High (75%)' },
|
||||
{ value: '100', label: 'Maximum (100%)' },
|
||||
{
|
||||
value: null,
|
||||
label: 'Need more freedom? Set a custom compression level through the API.',
|
||||
disabled: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
@@ -153,14 +138,13 @@ export function OptionsModal({
|
||||
description="The file name format to use when uploading this file. Leave blank for the server's default."
|
||||
value={state.format}
|
||||
onChange={(e) => setState({ format: e })}
|
||||
icon={<IconFileInfo size='1rem' />}
|
||||
icon={<TypeIcon />}
|
||||
data={[
|
||||
{ value: 'default', label: 'Default' },
|
||||
{ value: 'random', label: 'Random' },
|
||||
{ value: 'name', label: 'Original Name' },
|
||||
{ value: 'date', label: 'Date (format configured by server)' },
|
||||
{ value: 'uuid', label: 'UUID' },
|
||||
{ value: 'gfycat', label: 'Gfycat' },
|
||||
{ value: 'RANDOM', label: 'Random' },
|
||||
{ value: 'NAME', label: 'Original Name' },
|
||||
{ value: 'DATE', label: 'Date (format configured by server)' },
|
||||
{ value: 'UUID', label: 'UUID' },
|
||||
]}
|
||||
/>
|
||||
<PasswordInput
|
||||
@@ -168,12 +152,12 @@ export function OptionsModal({
|
||||
description='The password required to view this file. Leave blank for no password.'
|
||||
value={state.password}
|
||||
onChange={(e) => setState({ password: e.currentTarget.value })}
|
||||
icon={<IconKey size='1rem' />}
|
||||
icon={<KeyIcon />}
|
||||
/>
|
||||
<TextInput
|
||||
label='Override Domain'
|
||||
onChange={handleOD}
|
||||
icon={<IconWorld size='1rem' />}
|
||||
icon={<GlobeIcon />}
|
||||
description={odState.description}
|
||||
error={odState.error}
|
||||
/>
|
||||
@@ -210,11 +194,7 @@ export function OptionsModal({
|
||||
);
|
||||
}
|
||||
|
||||
export default function useUploadOptions(): [
|
||||
UploadOptionsState,
|
||||
Dispatch<SetStateAction<boolean>>,
|
||||
ReactNode
|
||||
] {
|
||||
export default function useUploadOptions(): [UploadOptionsState, Dispatch<SetStateAction<boolean>>, any] {
|
||||
const [state, setState] = useReducer((state, newState) => ({ ...state, ...newState }), {
|
||||
expires: 'never',
|
||||
password: '',
|
||||
|
||||
@@ -1,21 +1,59 @@
|
||||
import { ActionIcon, Card, Group, Stack, Title, Tooltip } from '@mantine/core';
|
||||
import { IconClipboardCopy, IconExternalLink, IconTrash } from '@tabler/icons-react';
|
||||
import AnchorNext from 'components/AnchorNext';
|
||||
import { ActionIcon, Card, Group, LoadingOverlay, Stack, Title, Tooltip } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { CopyIcon, CrossIcon, DeleteIcon, ExternalLinkIcon } from 'components/icons';
|
||||
import TrashIcon from 'components/icons/TrashIcon';
|
||||
import Link from 'components/Link';
|
||||
import MutedText from 'components/MutedText';
|
||||
import { URLResponse } from 'lib/queries/url';
|
||||
import { URLResponse, useURLDelete } from 'lib/queries/url';
|
||||
import { relativeTime } from 'lib/utils/client';
|
||||
|
||||
export default function URLCard({
|
||||
url,
|
||||
copyURL,
|
||||
deleteURL,
|
||||
}: {
|
||||
url: URLResponse;
|
||||
copyURL: (u: URLResponse) => void;
|
||||
deleteURL: (u: URLResponse) => void;
|
||||
}) {
|
||||
export default function URLCard({ url }: { url: URLResponse }) {
|
||||
const clipboard = useClipboard();
|
||||
const urlDelete = useURLDelete();
|
||||
|
||||
const copyURL = (u) => {
|
||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${u.url}`);
|
||||
if (!navigator.clipboard)
|
||||
showNotification({
|
||||
title: 'Unable to copy to clipboard',
|
||||
message: 'Zipline is unable to copy to clipboard due to security reasons.',
|
||||
color: 'red',
|
||||
});
|
||||
else
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: '',
|
||||
icon: <CopyIcon />,
|
||||
});
|
||||
};
|
||||
|
||||
const deleteURL = async (u) => {
|
||||
urlDelete.mutate(u.id, {
|
||||
onSuccess: () => {
|
||||
showNotification({
|
||||
title: 'Deleted URL',
|
||||
message: '',
|
||||
icon: <CrossIcon />,
|
||||
color: 'green',
|
||||
});
|
||||
},
|
||||
|
||||
onError: (url: any) => {
|
||||
showNotification({
|
||||
title: 'Failed to delete URL',
|
||||
message: url.error,
|
||||
icon: <DeleteIcon />,
|
||||
color: 'red',
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card key={url.id} sx={{ maxWidth: '100%' }} shadow='sm'>
|
||||
<LoadingOverlay visible={urlDelete.isLoading} />
|
||||
|
||||
<Group position='apart'>
|
||||
<Group position='left'>
|
||||
<Stack spacing={0}>
|
||||
@@ -35,19 +73,19 @@ export default function URLCard({
|
||||
)}
|
||||
<MutedText size='sm'>Views: {url.views}</MutedText>
|
||||
<MutedText size='sm'>
|
||||
URL: <AnchorNext href={url.destination}>{url.destination}</AnchorNext>
|
||||
URL: <Link href={url.destination}>{url.destination}</Link>
|
||||
</MutedText>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Stack>
|
||||
<ActionIcon href={url.url} component='a' target='_blank'>
|
||||
<IconExternalLink size='1rem' />
|
||||
<ExternalLinkIcon />
|
||||
</ActionIcon>
|
||||
<ActionIcon aria-label='copy' onClick={() => copyURL(url)}>
|
||||
<IconClipboardCopy size='1rem' />
|
||||
<CopyIcon />
|
||||
</ActionIcon>
|
||||
<ActionIcon aria-label='delete' onClick={() => deleteURL(url)}>
|
||||
<IconTrash size='1rem' />
|
||||
<TrashIcon />
|
||||
</ActionIcon>
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
@@ -8,71 +8,27 @@ import {
|
||||
NumberInput,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconClipboardCopy,
|
||||
IconExternalLink,
|
||||
IconGridDots,
|
||||
IconLink,
|
||||
IconLinkOff,
|
||||
IconList,
|
||||
} from '@tabler/icons-react';
|
||||
import AnchorNext from 'components/AnchorNext';
|
||||
import { CrossIcon, LinkIcon, PlusIcon } from 'components/icons';
|
||||
import MutedText from 'components/MutedText';
|
||||
import { ApiError } from 'hooks/useFetch';
|
||||
import { useURLDelete, useURLs } from 'lib/queries/url';
|
||||
import { listViewUrlsSelector } from 'lib/recoil/settings';
|
||||
import { useURLs } from 'lib/queries/url';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import URLCard from './URLCard';
|
||||
|
||||
export default function Urls() {
|
||||
const user = useRecoilValue(userSelector);
|
||||
|
||||
const modals = useModals();
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const urls = useURLs();
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
|
||||
const updateURLs = async () => urls.refetch();
|
||||
|
||||
const [listView, setListView] = useRecoilState(listViewUrlsSelector);
|
||||
|
||||
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
|
||||
columnAccessor: 'id',
|
||||
direction: 'asc',
|
||||
});
|
||||
const [records, setRecords] = useState(urls.data);
|
||||
|
||||
useEffect(() => {
|
||||
setRecords(urls.data);
|
||||
}, [urls.data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!records || records.length === 0) return;
|
||||
|
||||
const sortedRecords = [...records].sort((a, b) => {
|
||||
if (sortStatus.direction === 'asc') {
|
||||
return a[sortStatus.columnAccessor] > b[sortStatus.columnAccessor] ? 1 : -1;
|
||||
}
|
||||
|
||||
return a[sortStatus.columnAccessor] < b[sortStatus.columnAccessor] ? 1 : -1;
|
||||
});
|
||||
|
||||
setRecords(sortedRecords);
|
||||
}, [sortStatus]);
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
url: '',
|
||||
@@ -81,16 +37,6 @@ export default function Urls() {
|
||||
},
|
||||
});
|
||||
|
||||
const copy = (url) => {
|
||||
clipboard.copy(url);
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: <AnchorNext href={url}>{url}</AnchorNext>,
|
||||
color: 'green',
|
||||
icon: <IconClipboardCopy size='1rem' />,
|
||||
});
|
||||
};
|
||||
|
||||
const onSubmit = async (values) => {
|
||||
const cleanURL = values.url.trim();
|
||||
const cleanVanity = values.vanity.trim();
|
||||
@@ -132,31 +78,14 @@ export default function Urls() {
|
||||
title: 'Failed to create URL',
|
||||
message: json.error,
|
||||
color: 'red',
|
||||
icon: <IconLinkOff size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
} else {
|
||||
modals.openModal({
|
||||
title: <Title>Shortened URL!</Title>,
|
||||
size: 'auto',
|
||||
children: (
|
||||
<Group position='apart'>
|
||||
<Group position='left'>
|
||||
<AnchorNext href={json.url}>{data.vanity ?? json.url}</AnchorNext>
|
||||
</Group>
|
||||
<Group position='right'>
|
||||
<Tooltip label='Open link in a new tab'>
|
||||
<ActionIcon onClick={() => window.open(json.url, '_blank')} variant='filled' color='primary'>
|
||||
<IconExternalLink size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label='Copy link to clipboard'>
|
||||
<ActionIcon onClick={() => copy(json.url)} variant='filled' color='primary'>
|
||||
<IconClipboardCopy size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Group>
|
||||
),
|
||||
showNotification({
|
||||
title: 'URL shortened',
|
||||
message: json.url,
|
||||
color: 'green',
|
||||
icon: <LinkIcon />,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -167,52 +96,13 @@ export default function Urls() {
|
||||
updateURLs();
|
||||
}, []);
|
||||
|
||||
const copyURL = (u) => {
|
||||
clipboard.copy(`${window.location.protocol}//${window.location.host}${u.url}`);
|
||||
if (!navigator.clipboard)
|
||||
showNotification({
|
||||
title: 'Unable to copy to clipboard',
|
||||
message: 'Zipline is unable to copy to clipboard due to security reasons.',
|
||||
color: 'red',
|
||||
});
|
||||
else
|
||||
showNotification({
|
||||
title: 'Copied to clipboard',
|
||||
message: '',
|
||||
icon: <IconClipboardCopy size='1rem' />,
|
||||
});
|
||||
};
|
||||
|
||||
const urlDelete = useURLDelete();
|
||||
const deleteURL = async (u) => {
|
||||
urlDelete.mutate(u.id, {
|
||||
onSuccess: () => {
|
||||
showNotification({
|
||||
title: 'Deleted URL',
|
||||
message: '',
|
||||
icon: <IconLink size='1rem' />,
|
||||
color: 'green',
|
||||
});
|
||||
},
|
||||
|
||||
onError: (url: ApiError) => {
|
||||
showNotification({
|
||||
title: 'Failed to delete URL',
|
||||
message: url.error,
|
||||
icon: <IconLinkOff size='1rem' />,
|
||||
color: 'red',
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<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')} />
|
||||
<NumberInput id='maxViews' label='Max Views' {...form.getInputProps('maxViews')} min={0} />
|
||||
<NumberInput id='maxViews' label='Max Views' {...form.getInputProps('maxViews')} />
|
||||
|
||||
<Group position='right' mt='md'>
|
||||
<Button onClick={() => setCreateOpen(false)}>Cancel</Button>
|
||||
@@ -224,21 +114,16 @@ export default function Urls() {
|
||||
<Group mb='md'>
|
||||
<Title>URLs</Title>
|
||||
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}>
|
||||
<IconLink size='1rem' />
|
||||
<PlusIcon />
|
||||
</ActionIcon>
|
||||
<Tooltip label={listView ? 'Switch to grid view' : 'Switch to list view'}>
|
||||
<ActionIcon variant='filled' color='primary' onClick={() => setListView(!listView)}>
|
||||
{listView ? <IconList size='1rem' /> : <IconGridDots size='1rem' />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
|
||||
{!listView && urls.data && urls.data.length === 0 && (
|
||||
{urls.data && urls.data.length === 0 && (
|
||||
<Card shadow='md'>
|
||||
<Center>
|
||||
<Group>
|
||||
<div>
|
||||
<IconLink size={48} />
|
||||
<LinkIcon size={48} />
|
||||
</div>
|
||||
<div>
|
||||
<Title>Nothing here</Title>
|
||||
@@ -249,107 +134,11 @@ export default function Urls() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{listView ? (
|
||||
<DataTable
|
||||
withBorder
|
||||
borderRadius='md'
|
||||
highlightOnHover
|
||||
verticalSpacing='sm'
|
||||
columns={[
|
||||
{ accessor: 'id', title: 'ID', sortable: true },
|
||||
{
|
||||
accessor: 'vanity',
|
||||
title: 'Vanity',
|
||||
sortable: true,
|
||||
render: (url) => <Text>{url.vanity ?? ''}</Text>,
|
||||
},
|
||||
{
|
||||
accessor: 'destination',
|
||||
title: 'URL',
|
||||
sortable: true,
|
||||
render: (url) => (
|
||||
<AnchorNext href={url.url} target='_blank'>
|
||||
{url.destination}
|
||||
</AnchorNext>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessor: 'views',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
accessor: 'maxViews',
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
accessor: 'actions',
|
||||
textAlignment: 'right',
|
||||
render: (url) => (
|
||||
<Group spacing={4} position='right' noWrap>
|
||||
<Tooltip label='Open link in a new tab'>
|
||||
<ActionIcon
|
||||
onClick={() => window.open(url.url, '_blank')}
|
||||
variant='subtle'
|
||||
color='primary'
|
||||
>
|
||||
<IconExternalLink size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label='Copy link to clipboard'>
|
||||
<ActionIcon onClick={() => copyURL(url)} variant='subtle' color='primary'>
|
||||
<IconClipboardCopy size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label='Delete URL'>
|
||||
<ActionIcon onClick={() => deleteURL(url)} variant='subtle' color='red'>
|
||||
<IconLinkOff size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
]}
|
||||
sortStatus={sortStatus}
|
||||
onSortStatusChange={setSortStatus}
|
||||
records={records ?? []}
|
||||
fetching={urls.isLoading}
|
||||
loaderBackgroundBlur={5}
|
||||
minHeight='calc(100vh - 200px)'
|
||||
loaderVariant='dots'
|
||||
rowContextMenu={{
|
||||
shadow: 'xl',
|
||||
borderRadius: 'md',
|
||||
items: (url) => [
|
||||
{
|
||||
key: 'openLink',
|
||||
title: 'Open link in a new tab',
|
||||
icon: <IconExternalLink size='1rem' />,
|
||||
onClick: () => window.open(url.url, '_blank'),
|
||||
},
|
||||
{
|
||||
key: 'copyLink',
|
||||
title: 'Copy link to clipboard',
|
||||
icon: <IconClipboardCopy size='1rem' />,
|
||||
onClick: () => copyURL(url),
|
||||
},
|
||||
{
|
||||
key: 'deleteURL',
|
||||
title: 'Delete URL',
|
||||
icon: <IconLinkOff size='1rem' />,
|
||||
onClick: () => deleteURL(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} deleteURL={deleteURL} copyURL={copyURL} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, Group, Modal, Switch, TextInput, Title } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconUserPlus, IconUserX } from '@tabler/icons-react';
|
||||
import { DeleteIcon, PlusIcon } from 'components/icons';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
|
||||
export function CreateUserModal({ open, setOpen, updateUsers }) {
|
||||
@@ -31,14 +31,14 @@ export function CreateUserModal({ open, setOpen, updateUsers }) {
|
||||
showNotification({
|
||||
title: 'Failed to create user',
|
||||
message: res.error,
|
||||
icon: <IconUserX size='1rem' />,
|
||||
icon: <DeleteIcon />,
|
||||
color: 'red',
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Created user: ' + cleanUsername,
|
||||
message: '',
|
||||
icon: <IconUserPlus size='1rem' />,
|
||||
icon: <PlusIcon />,
|
||||
color: 'green',
|
||||
});
|
||||
}
|
||||
@@ -55,9 +55,7 @@ export function CreateUserModal({ open, setOpen, updateUsers }) {
|
||||
|
||||
<Group position='right' mt='md'>
|
||||
<Button onClick={() => setOpen(false)}>Cancel</Button>
|
||||
<Button type='submit' rightIcon={<IconUserPlus size='1rem' />}>
|
||||
Create
|
||||
</Button>
|
||||
<Button type='submit'>Create</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, Group, Modal, Switch, TextInput, Title } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconUserCheck, IconUserExclamation } from '@tabler/icons-react';
|
||||
import { DeleteIcon, PlusIcon } from 'components/icons';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
|
||||
export function EditUserModal({ open, setOpen, updateUsers, user }) {
|
||||
@@ -36,14 +36,14 @@ export function EditUserModal({ open, setOpen, updateUsers, user }) {
|
||||
showNotification({
|
||||
title: 'Failed to edit user',
|
||||
message: res.error,
|
||||
icon: <IconUserExclamation size='1rem' />,
|
||||
icon: <DeleteIcon />,
|
||||
color: 'red',
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'Edited user: ' + cleanUsername,
|
||||
message: '',
|
||||
icon: <IconUserCheck size='1rem' />,
|
||||
icon: <PlusIcon />,
|
||||
color: 'green',
|
||||
});
|
||||
}
|
||||
@@ -52,11 +52,7 @@ export function EditUserModal({ open, setOpen, updateUsers, user }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={open}
|
||||
onClose={() => setOpen(false)}
|
||||
title={<Title>Edit "{user?.username}"</Title>}
|
||||
>
|
||||
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>Edit User {user?.username}</Title>}>
|
||||
{user && (
|
||||
<form onSubmit={form.onSubmit((v) => onSubmit(v))}>
|
||||
<TextInput id='username' label='Username' {...form.getInputProps('username')} />
|
||||
|
||||
@@ -1,25 +1,13 @@
|
||||
import { ActionIcon, Avatar, Card, Group, SimpleGrid, Skeleton, Stack, Title, Tooltip } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { ActionIcon, Avatar, Card, Group, SimpleGrid, Skeleton, Stack, Title } from '@mantine/core';
|
||||
import { useModals } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import type { User } from '@prisma/client';
|
||||
import {
|
||||
IconClipboardCopy,
|
||||
IconEdit,
|
||||
IconGridDots,
|
||||
IconList,
|
||||
IconUserExclamation,
|
||||
IconUserMinus,
|
||||
IconUserPlus,
|
||||
} from '@tabler/icons-react';
|
||||
import { CrossIcon, DeleteIcon, PencilIcon, PlusIcon } from 'components/icons';
|
||||
import MutedText from 'components/MutedText';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import { listViewUsersSelector } from 'lib/recoil/settings';
|
||||
import { userSelector } from 'lib/recoil/user';
|
||||
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { CreateUserModal } from './CreateUserModal';
|
||||
import { EditUserModal } from './EditUserModal';
|
||||
|
||||
@@ -27,39 +15,12 @@ export default function Users() {
|
||||
const self = useRecoilValue(userSelector);
|
||||
const router = useRouter();
|
||||
const modals = useModals();
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [users, setUsers] = useState([]);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [selectedUser, setSelectedUser] = useState(null);
|
||||
|
||||
const [listView, setListView] = useRecoilState(listViewUsersSelector);
|
||||
|
||||
const [sortStatus, setSortStatus] = useState<DataTableSortStatus>({
|
||||
columnAccessor: 'id',
|
||||
direction: 'asc',
|
||||
});
|
||||
const [records, setRecords] = useState(users);
|
||||
|
||||
useEffect(() => {
|
||||
setRecords(users);
|
||||
}, [users]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!records || records.length === 0) return;
|
||||
|
||||
const sortedRecords = [...records].sort((a, b) => {
|
||||
if (sortStatus.direction === 'asc') {
|
||||
return a[sortStatus.columnAccessor] > b[sortStatus.columnAccessor] ? 1 : -1;
|
||||
}
|
||||
|
||||
return a[sortStatus.columnAccessor] < b[sortStatus.columnAccessor] ? 1 : -1;
|
||||
});
|
||||
|
||||
setRecords(sortedRecords);
|
||||
}, [sortStatus]);
|
||||
|
||||
const handleDelete = async (user, delete_files) => {
|
||||
const res = await useFetch(`/api/user/${user.id}`, 'DELETE', {
|
||||
delete_files,
|
||||
@@ -70,14 +31,14 @@ export default function Users() {
|
||||
title: 'Failed to delete user',
|
||||
message: res.error,
|
||||
color: 'red',
|
||||
icon: <IconUserExclamation size='1rem' />,
|
||||
icon: <CrossIcon />,
|
||||
});
|
||||
} else {
|
||||
showNotification({
|
||||
title: 'User deleted',
|
||||
message: '',
|
||||
color: 'green',
|
||||
icon: <IconUserMinus size='1rem' />,
|
||||
icon: <DeleteIcon />,
|
||||
});
|
||||
updateUsers();
|
||||
}
|
||||
@@ -94,7 +55,7 @@ export default function Users() {
|
||||
title: `Delete ${user.username}'s files?`,
|
||||
labels: { confirm: 'Yes', cancel: 'No' },
|
||||
centered: true,
|
||||
overlayProps: { blur: 3 },
|
||||
overlayBlur: 3,
|
||||
onConfirm: () => {
|
||||
handleDelete(user, true);
|
||||
modals.closeAll();
|
||||
@@ -128,150 +89,53 @@ export default function Users() {
|
||||
<Group mb='md'>
|
||||
<Title>Users</Title>
|
||||
<ActionIcon variant='filled' color='primary' onClick={() => setCreateOpen(true)}>
|
||||
<IconUserPlus size='1rem' />
|
||||
<PlusIcon />
|
||||
</ActionIcon>
|
||||
<Tooltip label={listView ? 'Switch to grid view' : 'Switch to list view'}>
|
||||
<ActionIcon variant='filled' color='primary' onClick={() => setListView(!listView)}>
|
||||
{listView ? <IconList size='1rem' /> : <IconGridDots size='1rem' />}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
{listView ? (
|
||||
<DataTable
|
||||
withBorder
|
||||
borderRadius='md'
|
||||
highlightOnHover
|
||||
verticalSpacing='sm'
|
||||
columns={[
|
||||
{
|
||||
accessor: 'avatar',
|
||||
sortable: false,
|
||||
render: (user) => (
|
||||
<Avatar src={user.avatar} color={user.administrator ? 'primary' : 'dark'} size='md'>
|
||||
{user.username[0]}
|
||||
</Avatar>
|
||||
),
|
||||
width: 80,
|
||||
},
|
||||
{ accessor: 'id', title: 'ID', sortable: true },
|
||||
{ accessor: 'username', sortable: true },
|
||||
{
|
||||
accessor: 'administrator',
|
||||
sortable: true,
|
||||
render: (user) => (user.administrator ? 'Yes' : 'No'),
|
||||
},
|
||||
{
|
||||
accessor: 'actions',
|
||||
textAlignment: 'right',
|
||||
render: (user) => (
|
||||
<Group spacing={4} position='right' noWrap>
|
||||
<Tooltip label='Delete user'>
|
||||
<ActionIcon onClick={() => openDeleteModal(user)} color='red'>
|
||||
<IconUserMinus size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label='Edit user'>
|
||||
<ActionIcon
|
||||
onClick={() => {
|
||||
setSelectedUser(user);
|
||||
setEditOpen(true);
|
||||
}}
|
||||
color='blue'
|
||||
>
|
||||
<IconEdit size='1rem' />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
),
|
||||
},
|
||||
]}
|
||||
sortStatus={sortStatus}
|
||||
onSortStatusChange={setSortStatus}
|
||||
records={records ? records.filter((x) => x.username !== self.username) : []}
|
||||
fetching={users.length === 0}
|
||||
loaderBackgroundBlur={5}
|
||||
minHeight='calc(100vh - 200px)'
|
||||
loaderVariant='dots'
|
||||
rowContextMenu={{
|
||||
shadow: 'xl',
|
||||
borderRadius: 'md',
|
||||
items: (user) => [
|
||||
{
|
||||
key: 'copy',
|
||||
icon: <IconClipboardCopy size='1rem' />,
|
||||
title: `Copy Username: "${user.username}"`,
|
||||
onClick: () => clipboard.copy(user.username),
|
||||
},
|
||||
{
|
||||
key: 'edit',
|
||||
icon: <IconEdit size='1rem' />,
|
||||
title: `Edit ${user.username}`,
|
||||
onClick: () => {
|
||||
setSelectedUser(user);
|
||||
setEditOpen(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
icon: <IconUserMinus size='1rem' />,
|
||||
title: `Delete ${user.username}`,
|
||||
onClick: () => openDeleteModal(user),
|
||||
},
|
||||
],
|
||||
}}
|
||||
onCellClick={({ column, record: user }) => {
|
||||
if (column.accessor === 'actions') return;
|
||||
|
||||
setSelectedUser(user);
|
||||
setEditOpen(true);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
|
||||
{users.length
|
||||
? users
|
||||
.filter((x) => x.username !== self.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>
|
||||
<Stack>
|
||||
{user.administrator && !self.superAdmin ? null : (
|
||||
<>
|
||||
<ActionIcon
|
||||
aria-label='edit'
|
||||
onClick={() => {
|
||||
setEditOpen(true);
|
||||
setSelectedUser(user);
|
||||
}}
|
||||
>
|
||||
<IconEdit size='1rem' />
|
||||
</ActionIcon>
|
||||
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(user)}>
|
||||
<IconUserMinus size='1rem' />
|
||||
</ActionIcon>
|
||||
</>
|
||||
)}
|
||||
<SimpleGrid cols={3} spacing='lg' breakpoints={[{ maxWidth: 'sm', cols: 1, spacing: 'sm' }]}>
|
||||
{users.length
|
||||
? users
|
||||
.filter((x) => x.username !== self.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>
|
||||
</Card>
|
||||
))
|
||||
: [1, 2, 3].map((x) => <Skeleton key={x} width='100%' height={100} radius='sm' />)}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
<Stack>
|
||||
{user.administrator && !self.superAdmin ? null : (
|
||||
<>
|
||||
<ActionIcon
|
||||
aria-label='edit'
|
||||
onClick={() => {
|
||||
setEditOpen(true);
|
||||
setSelectedUser(user);
|
||||
}}
|
||||
>
|
||||
<PencilIcon />
|
||||
</ActionIcon>
|
||||
<ActionIcon aria-label='delete' onClick={() => openDeleteModal(user)}>
|
||||
<DeleteIcon />
|
||||
</ActionIcon>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Group>
|
||||
</Card>
|
||||
))
|
||||
: [1, 2, 3].map((x) => <Skeleton key={x} width='100%' height={100} radius='sm' />)}
|
||||
</SimpleGrid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Alert } from '@mantine/core';
|
||||
import type { ParseError } from 'katex';
|
||||
import katex, { ParseError } from 'katex';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import 'katex/dist/katex.min.css';
|
||||
@@ -21,24 +21,21 @@ export default function KaTeX({ code, ...props }) {
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const katex = await import('katex');
|
||||
try {
|
||||
const html = katex.default.renderToString(code, {
|
||||
displayMode: true,
|
||||
throwOnError: true,
|
||||
errorColor: '#f44336',
|
||||
});
|
||||
try {
|
||||
const html = katex.renderToString(code, {
|
||||
displayMode: true,
|
||||
throwOnError: true,
|
||||
errorColor: '#f44336',
|
||||
});
|
||||
|
||||
setRendered(html);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(renderError(e));
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
setRendered(html);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(renderError(e));
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [rendered, error, code]);
|
||||
|
||||
if (error) return error;
|
||||
|
||||
@@ -2,24 +2,24 @@ import { Code } from '@mantine/core';
|
||||
import { Prism } from '@mantine/prism';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { Language } from 'prism-react-renderer';
|
||||
|
||||
export default function Markdown({ code, ...props }) {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={{
|
||||
code({ inline, className, children, ...props }) {
|
||||
code({ node, inline, className, children, ...props }) {
|
||||
const match = /language-(\w+)/.exec(className || '');
|
||||
return !inline && match ? (
|
||||
<Prism language={match[1] as Language} {...props}>
|
||||
// @ts-ignore
|
||||
<Prism language={match[1]} {...props}>
|
||||
{String(children).replace(/\n$/, '')}
|
||||
</Prism>
|
||||
) : (
|
||||
<Code {...props}>{children}</Code>
|
||||
);
|
||||
},
|
||||
img(props) {
|
||||
img({ node, ...props }) {
|
||||
return <img {...props} style={{ maxWidth: '100%' }} />;
|
||||
},
|
||||
}}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user