mirror of
https://github.com/diced/zipline.git
synced 2025-05-10 18:05:54 +02:00
fix: a bunch of random stuff
This commit is contained in:
parent
dcb4a4e9e7
commit
12fcff1a14
82 changed files with 9015 additions and 5875 deletions
45
.eslintrc.js
45
.eslintrc.js
|
@ -1,45 +0,0 @@
|
|||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: ['next/core-web-vitals', 'plugin:prettier/recommended', 'plugin:@typescript-eslint/recommended'],
|
||||
root: true,
|
||||
plugins: ['unused-imports', '@typescript-eslint'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
rules: {
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
quotes: [
|
||||
'error',
|
||||
'single',
|
||||
{
|
||||
avoidEscape: true,
|
||||
},
|
||||
],
|
||||
semi: ['error', 'always'],
|
||||
// 'comma-dangle': ['error', 'always-multiline'],
|
||||
'jsx-quotes': ['error', 'prefer-single'],
|
||||
indent: 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react/jsx-uses-react': 'warn',
|
||||
'react/jsx-uses-vars': 'warn',
|
||||
'react/no-danger-with-children': 'warn',
|
||||
'react/no-deprecated': 'warn',
|
||||
'react/no-direct-mutation-state': 'warn',
|
||||
'react/no-is-mounted': 'warn',
|
||||
'react/no-typos': 'error',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/require-render-return': 'error',
|
||||
'react/style-prop-object': 'warn',
|
||||
'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',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
};
|
86
eslint.config.mjs
Normal file
86
eslint.config.mjs
Normal file
|
@ -0,0 +1,86 @@
|
|||
// TODO: migrate everything to use eslint 9 features instead of compatibility layers
|
||||
|
||||
import unusedImports from 'eslint-plugin-unused-imports';
|
||||
import typescriptEslint from '@typescript-eslint/eslint-plugin';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import js from '@eslint/js';
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
import { includeIgnoreFile } from '@eslint/compat';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
const gitignorePath = path.resolve(__dirname, '.gitignore');
|
||||
|
||||
export default [
|
||||
includeIgnoreFile(gitignorePath),
|
||||
...compat.extends(
|
||||
'next/core-web-vitals',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
),
|
||||
{
|
||||
plugins: {
|
||||
'unused-imports': unusedImports,
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
},
|
||||
|
||||
rules: {
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
|
||||
quotes: [
|
||||
'error',
|
||||
'single',
|
||||
{
|
||||
avoidEscape: true,
|
||||
},
|
||||
],
|
||||
|
||||
semi: ['error', 'always'],
|
||||
'jsx-quotes': ['error', 'prefer-single'],
|
||||
indent: 'off',
|
||||
'react/prop-types': 'off',
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react/jsx-uses-react': 'warn',
|
||||
'react/jsx-uses-vars': 'warn',
|
||||
'react/no-danger-with-children': 'warn',
|
||||
'react/no-deprecated': 'warn',
|
||||
'react/no-direct-mutation-state': 'warn',
|
||||
'react/no-is-mounted': 'warn',
|
||||
'react/no-typos': 'error',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'react/require-render-return': 'error',
|
||||
'react/style-prop-object': 'warn',
|
||||
'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': [
|
||||
'warn',
|
||||
{
|
||||
vars: 'all',
|
||||
varsIgnorePattern: '^_',
|
||||
args: 'after-used',
|
||||
argsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-unused-expressions': 'off',
|
||||
},
|
||||
},
|
||||
];
|
117
package.json
117
package.json
|
@ -8,99 +8,100 @@
|
|||
"build:prisma": "prisma generate",
|
||||
"build:next": "next build",
|
||||
"build:server": "tsup",
|
||||
"dev": "NODE_ENV=development DEBUG=zipline tsx --require dotenv/config --enable-source-maps ./src/server",
|
||||
"dev:inspector": "NODE_ENV=development DEBUG=zipline tsx --require dotenv/config --inspect=0.0.0.0:9229 --enable-source-maps ./src/server",
|
||||
"dev:ctl": "tsup --config tsup.ctl.config.ts --watch",
|
||||
"start": "NODE_ENV=production node --require dotenv/config --enable-source-maps ./build/server",
|
||||
"dev": "TURBOPACK=1 NODE_ENV=development DEBUG=zipline tsx --require dotenv/config --enable-source-maps ./src/server",
|
||||
"dev:inspector": "TURBOPACK=1 NODE_ENV=development DEBUG=zipline tsx --require dotenv/config --inspect=0.0.0.0:9229 --enable-source-maps ./src/server",
|
||||
"start": "NODE_ENV=production node --trace-warnings --require dotenv/config --enable-source-maps ./build/server",
|
||||
"start:inspector": "NODE_ENV=production node --require dotenv/config --inspect=0.0.0.0:9229 --enable-source-maps ./build/server",
|
||||
"ctl": "NODE_ENV=production node --require dotenv/config --enable-source-maps ./build/ctl",
|
||||
"validate": "pnpm run \"/^validate:.*/\"",
|
||||
"validate:lint": "eslint --cache --ignore-path .gitignore --fix .",
|
||||
"validate:lint": "eslint --cache --fix .",
|
||||
"validate:format": "prettier --write --ignore-path .gitignore .",
|
||||
"db:prototype": "prisma db push --skip-generate && prisma generate --no-hints"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/plots": "^1.2.6",
|
||||
"@aws-sdk/client-s3": "^3.654.0",
|
||||
"@fastify/cookie": "^9.3.1",
|
||||
"@aws-sdk/client-s3": "^3.714.0",
|
||||
"@fastify/cookie": "^9.4.0",
|
||||
"@fastify/cors": "^9.0.1",
|
||||
"@fastify/multipart": "^8.2.0",
|
||||
"@fastify/multipart": "^8.3.0",
|
||||
"@fastify/rate-limit": "^9.1.0",
|
||||
"@fastify/sensible": "^5.5.0",
|
||||
"@fastify/sensible": "^5.6.0",
|
||||
"@fastify/static": "^7.0.4",
|
||||
"@github/webauthn-json": "^2.1.1",
|
||||
"@mantine/code-highlight": "^7.12.2",
|
||||
"@mantine/core": "^7.12.2",
|
||||
"@mantine/dates": "^7.12.2",
|
||||
"@mantine/dropzone": "^7.12.2",
|
||||
"@mantine/form": "^7.12.2",
|
||||
"@mantine/hooks": "^7.12.2",
|
||||
"@mantine/modals": "^7.12.2",
|
||||
"@mantine/notifications": "^7.12.2",
|
||||
"@prisma/client": "^5.19.1",
|
||||
"@prisma/internals": "^5.19.1",
|
||||
"@prisma/migrate": "^5.19.1",
|
||||
"@tabler/icons-react": "^2.47.0",
|
||||
"@mantine/charts": "^7.15.1",
|
||||
"@mantine/code-highlight": "^7.15.1",
|
||||
"@mantine/core": "^7.15.1",
|
||||
"@mantine/dates": "^7.15.1",
|
||||
"@mantine/dropzone": "^7.15.1",
|
||||
"@mantine/form": "^7.15.1",
|
||||
"@mantine/hooks": "^7.15.1",
|
||||
"@mantine/modals": "^7.15.1",
|
||||
"@mantine/notifications": "^7.15.1",
|
||||
"@prisma/client": "^6.1.0",
|
||||
"@prisma/internals": "^6.1.0",
|
||||
"@prisma/migrate": "^6.1.0",
|
||||
"@tabler/icons-react": "^3.26.0",
|
||||
"@xoi/gps-metadata-remover": "^1.1.2",
|
||||
"argon2": "^0.30.3",
|
||||
"argon2": "^0.41.1",
|
||||
"bytes": "^3.1.2",
|
||||
"clsx": "^2.1.1",
|
||||
"colorette": "^2.0.20",
|
||||
"commander": "^12.1.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.18.2",
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fastify": "^4.28.1",
|
||||
"fastify": "^4.29.0",
|
||||
"fastify-plugin": "^4.5.1",
|
||||
"fflate": "^0.8.2",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"highlight.js": "^11.10.0",
|
||||
"iron-session": "^8.0.3",
|
||||
"isomorphic-dompurify": "^1.13.0",
|
||||
"katex": "^0.16.11",
|
||||
"mantine-datatable": "^7.12.4",
|
||||
"highlight.js": "^11.11.0",
|
||||
"iron-session": "^8.0.4",
|
||||
"isomorphic-dompurify": "^2.19.0",
|
||||
"katex": "^0.16.17",
|
||||
"mantine-datatable": "^7.14.5",
|
||||
"ms": "^2.1.3",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"next": "^14.2.13",
|
||||
"next": "^15.1.1",
|
||||
"otplib": "^12.0.1",
|
||||
"prisma": "^5.19.1",
|
||||
"prisma": "^6.1.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-markdown": "^8.0.7",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"sharp": "^0.32.6",
|
||||
"react": "^19.0.0-rc.1",
|
||||
"react-dom": "^19.0.0-rc.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sharp": "^0.33.5",
|
||||
"swr": "^2.2.5",
|
||||
"znv": "^0.3.2",
|
||||
"zod": "^3.23.8",
|
||||
"zustand": "^4.5.5"
|
||||
"zod": "^3.24.1",
|
||||
"zustand": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bytes": "^3.1.4",
|
||||
"@eslint/compat": "^1.2.4",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@types/bytes": "^3.1.5",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/fluent-ffmpeg": "^2.1.26",
|
||||
"@types/fluent-ffmpeg": "^2.1.27",
|
||||
"@types/katex": "^0.16.7",
|
||||
"@types/ms": "^0.7.34",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/node": "^20.16.5",
|
||||
"@types/node": "^20.17.10",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^18.3.8",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.1",
|
||||
"@typescript-eslint/parser": "^6.13.1",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-next": "^13.5.6",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-unused-imports": "^3.0.0",
|
||||
"postcss": "^8.4.47",
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.1",
|
||||
"@typescript-eslint/parser": "^8.18.1",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-config-next": "^15.1.1",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"eslint-plugin-unused-imports": "^4.1.4",
|
||||
"postcss": "^8.4.49",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "^3.3.3",
|
||||
"tsup": "^7.2.0",
|
||||
"tsx": "^4.19.1",
|
||||
"typescript": "^5.6.2"
|
||||
"prettier": "^3.4.2",
|
||||
"tsup": "^8.3.5",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
|
13732
pnpm-lock.yaml
generated
13732
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
@ -1,9 +1,10 @@
|
|||
import { ViewStore, ViewType, useViewStore } from '@/lib/store/view';
|
||||
import { Center, SegmentedControl } from '@mantine/core';
|
||||
import { IconLayoutGrid, IconLayoutList } from '@tabler/icons-react';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
export default function GridTableSwitcher({ type }: { type: Exclude<keyof ViewStore, 'setView'> }) {
|
||||
const [view, setView] = useViewStore((state) => [state[type], state.setView]);
|
||||
const [view, setView] = useViewStore(useShallow((state) => [state[type], state.setView]));
|
||||
|
||||
return (
|
||||
<SegmentedControl
|
||||
|
|
|
@ -146,18 +146,14 @@ export default function Layout({ children, config }: { children: React.ReactNode
|
|||
const router = useRouter();
|
||||
const modals = useModals();
|
||||
const clipboard = useClipboard();
|
||||
const [setUser] = useUserStore((s) => [s.setUser]);
|
||||
const setUser = useUserStore((s) => s.setUser);
|
||||
|
||||
const { user, mutate } = useLogin();
|
||||
const { avatar } = useAvatar();
|
||||
|
||||
const copyToken = () => {
|
||||
modals.openConfirmModal({
|
||||
title: (
|
||||
<Title order={4} fw={700}>
|
||||
Copy token?
|
||||
</Title>
|
||||
),
|
||||
title: 'Copy token?',
|
||||
children:
|
||||
'Are you sure you want to copy your token? Your token can interact with all parts of Zipline. Do not share this token with anyone.',
|
||||
labels: { confirm: 'Copy', cancel: 'No, close this popup' },
|
||||
|
@ -185,11 +181,7 @@ export default function Layout({ children, config }: { children: React.ReactNode
|
|||
|
||||
const refreshToken = () => {
|
||||
modals.openConfirmModal({
|
||||
title: (
|
||||
<Title order={4} fw={700}>
|
||||
Refresh token?
|
||||
</Title>
|
||||
),
|
||||
title: 'Refresh token?',
|
||||
|
||||
children:
|
||||
'Are you sure you want to refresh your token? Once you refresh/reset your token, you will need to update any scripts or applications that use your token.',
|
||||
|
|
|
@ -5,6 +5,7 @@ import { ZiplineTheme, findTheme, themeComponents } from '@/lib/theme';
|
|||
import { MantineProvider, createTheme } from '@mantine/core';
|
||||
import { useColorScheme } from '@mantine/hooks';
|
||||
import { createContext, useContext } from 'react';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
const ThemeContext = createContext<{
|
||||
themes: ZiplineTheme[];
|
||||
|
@ -29,11 +30,9 @@ export default function Theming({
|
|||
defaultTheme?: Config['website']['theme'];
|
||||
}) {
|
||||
const user = useUserStore((state) => state.user);
|
||||
const [userTheme, preferredDark, preferredLight] = useSettingsStore((state) => [
|
||||
state.settings.theme,
|
||||
state.settings.themeDark,
|
||||
state.settings.themeLight,
|
||||
]);
|
||||
const [userTheme, preferredDark, preferredLight] = useSettingsStore(
|
||||
useShallow((state) => [state.settings.theme, state.settings.themeDark, state.settings.themeLight]),
|
||||
);
|
||||
const systemTheme = useColorScheme();
|
||||
const currentTheme = user ? userTheme : (defaultTheme?.default ?? 'system');
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { File } from '@/lib/db/models/file';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { Button, Divider, Modal, NumberInput, PasswordInput, Stack, TextInput, Title } from '@mantine/core';
|
||||
import { Button, Divider, Modal, NumberInput, PasswordInput, Stack, TextInput } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconEye, IconKey, IconPencil, IconPencilOff, IconTrashFilled } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
|
@ -86,12 +86,7 @@ export default function EditFileDetailsModal({
|
|||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
zIndex={300}
|
||||
title={<Title>Editing "{file.name}"</Title>}
|
||||
onClose={onClose}
|
||||
opened={open}
|
||||
>
|
||||
<Modal zIndex={300} title={`Editing "${file.name}"`} onClose={onClose} opened={open}>
|
||||
<Stack gap='xs' my='sm'>
|
||||
<NumberInput
|
||||
label='Max Views'
|
||||
|
|
|
@ -184,9 +184,9 @@ export default function FileModal({
|
|||
opened={open}
|
||||
onClose={() => setOpen(false)}
|
||||
title={
|
||||
<Title order={3} fw={700}>
|
||||
<Text size='xl' fw={700}>
|
||||
{file?.name ?? ''}
|
||||
</Title>
|
||||
</Text>
|
||||
}
|
||||
size='auto'
|
||||
centered
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import DashboardFile from '@/components/file/DashboardFile';
|
||||
import Stat from '@/components/Stat';
|
||||
import type { Response } from '@/lib/api/response';
|
||||
import { bytes } from '@/lib/bytes';
|
||||
import useLogin from '@/lib/hooks/useLogin';
|
||||
import { Paper, ScrollArea, SimpleGrid, Skeleton, Table, Text, Title } from '@mantine/core';
|
||||
import { IconDeviceSdCard, IconEyeFilled, IconFiles, IconLink, IconStarFilled } from '@tabler/icons-react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import useSWR from 'swr';
|
||||
|
||||
const DashboardFile = dynamic(() => import('@/components/file/DashboardFile'), {
|
||||
loading: () => <Skeleton height={350} animate />,
|
||||
});
|
||||
|
||||
export default function DashboardHome() {
|
||||
const { user } = useLogin();
|
||||
const { data: recent, isLoading: recentLoading } = useSWR<Response['/api/user/recent']>('/api/user/recent');
|
||||
|
@ -14,7 +18,7 @@ export default function DashboardHome() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Title order={1}>
|
||||
<Title>
|
||||
Welcome back, <b>{user?.username}</b>
|
||||
</Title>
|
||||
|
||||
|
@ -52,14 +56,14 @@ export default function DashboardHome() {
|
|||
|
||||
{recentLoading ? (
|
||||
<SimpleGrid cols={{ base: 1, md: 2, lg: 3 }} spacing={{ base: 'sm', md: 'md' }}>
|
||||
{[...Array(3)].map((i) => (
|
||||
<Skeleton key={i} height={350} />
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<Skeleton key={i} height={350} animate />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : recent?.length !== 0 ? (
|
||||
<SimpleGrid cols={{ base: 1, md: 2, lg: 3 }} spacing={{ base: 'sm', md: 'md' }}>
|
||||
{recent!.map((file) => (
|
||||
<DashboardFile key={file.id} file={file} />
|
||||
{recent!.map((file, i) => (
|
||||
<DashboardFile key={i} file={file} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : (
|
||||
|
@ -78,7 +82,7 @@ export default function DashboardHome() {
|
|||
{statsLoading ? (
|
||||
<>
|
||||
<SimpleGrid cols={{ base: 1, md: 2, lg: 4 }} spacing={{ base: 'sm', md: 'md' }}>
|
||||
{[...Array(8)].map((i) => (
|
||||
{[...Array(8)].map((_, i) => (
|
||||
<Skeleton key={i} height={105} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
|
@ -97,7 +101,7 @@ export default function DashboardHome() {
|
|||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{[1, 2, 3, 4, 5].map((i) => (
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Table.Tr key={i}>
|
||||
<Table.Td>
|
||||
<Skeleton animate>
|
||||
|
@ -147,8 +151,8 @@ export default function DashboardHome() {
|
|||
<Table.Tbody>
|
||||
{Object.entries(stats!.sortTypeCount)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.map(([type, count]) => (
|
||||
<Table.Tr key={type}>
|
||||
.map(([type, count], i) => (
|
||||
<Table.Tr key={i}>
|
||||
<Table.Td>{type}</Table.Td>
|
||||
<Table.Td>{count}</Table.Td>
|
||||
</Table.Tr>
|
||||
|
|
|
@ -1,19 +1,7 @@
|
|||
import { Response } from '@/lib/api/response';
|
||||
import { IncompleteFile } from '@/lib/db/models/incompleteFile';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import {
|
||||
ActionIcon,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Modal,
|
||||
Paper,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { ActionIcon, Badge, Button, Card, Group, Modal, Paper, Stack, Text, Tooltip } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IncompleteFileStatus } from '@prisma/client';
|
||||
import { IconFileDots, IconTrashFilled } from '@tabler/icons-react';
|
||||
|
@ -91,7 +79,7 @@ export default function PendingFilesButton() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>Pending Files</Title>}>
|
||||
<Modal opened={open} onClose={() => setOpen(false)} title='Pending Files'>
|
||||
<Stack gap='xs'>
|
||||
{incompleteFiles?.map((incompleteFile) => (
|
||||
<Card key={incompleteFile.id} withBorder>
|
||||
|
|
|
@ -2,7 +2,6 @@ import { mutateFiles } from '@/components/file/actions';
|
|||
import { Response } from '@/lib/api/response';
|
||||
import { File } from '@/lib/db/models/file';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { Title } from '@mantine/core';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconFilesOff, IconStarsFilled, IconStarsOff, IconTrashFilled } from '@tabler/icons-react';
|
||||
|
@ -10,11 +9,7 @@ import { IconFilesOff, IconStarsFilled, IconStarsOff, IconTrashFilled } from '@t
|
|||
export async function bulkDelete(ids: string[], setSelectedFiles: (files: File[]) => void) {
|
||||
modals.openConfirmModal({
|
||||
centered: true,
|
||||
title: (
|
||||
<Title>
|
||||
Delete {ids.length} file{ids.length === 1 ? '' : 's'}?
|
||||
</Title>
|
||||
),
|
||||
title: `Delete ${ids.length} file${ids.length === 1 ? '' : 's'}?`,
|
||||
children: `You are about to delete ${ids.length} file${
|
||||
ids.length === 1 ? '' : 's'
|
||||
}. This action cannot be undone.`,
|
||||
|
@ -76,11 +71,7 @@ export async function bulkDelete(ids: string[], setSelectedFiles: (files: File[]
|
|||
export async function bulkFavorite(ids: string[]) {
|
||||
modals.openConfirmModal({
|
||||
centered: true,
|
||||
title: (
|
||||
<Title>
|
||||
Favorite {ids.length} file{ids.length === 1 ? '' : 's'}?
|
||||
</Title>
|
||||
),
|
||||
title: `Favorite ${ids.length} file${ids.length === 1 ? '' : 's'}?`,
|
||||
children: `You are about to favorite ${ids.length} file${ids.length === 1 ? '' : 's'}.`,
|
||||
labels: {
|
||||
cancel: 'Cancel',
|
||||
|
|
|
@ -2,8 +2,8 @@ import { Response } from '@/lib/api/response';
|
|||
import { Tag } from '@/lib/db/models/tag';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { colorHash } from '@/lib/theme/color';
|
||||
import { ActionIcon, Button, ColorInput, Modal, Stack, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { hasLength, useForm } from '@mantine/form';
|
||||
import { ActionIcon, Button, ColorInput, Modal, Stack, TextInput, Tooltip } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconTag, IconTagOff, IconTextRecognition } from '@tabler/icons-react';
|
||||
import { mutate } from 'swr';
|
||||
|
@ -18,7 +18,7 @@ export default function CreateTagModal({ open, onClose }: { open: boolean; onClo
|
|||
color: '',
|
||||
},
|
||||
validate: {
|
||||
name: hasLength({ min: 1 }, 'Name is required'),
|
||||
name: (value) => (value.length < 1 ? 'Name is required' : null),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -60,7 +60,7 @@ export default function CreateTagModal({ open, onClose }: { open: boolean; onClo
|
|||
};
|
||||
|
||||
return (
|
||||
<Modal opened={open} onClose={onClose} title={<Title>Create new tag</Title>} zIndex={3000}>
|
||||
<Modal opened={open} onClose={onClose} title='Create new tag' zIndex={3000}>
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='sm'>
|
||||
<TextInput label='Name' placeholder='Enter a name...' {...form.getInputProps('name')} />
|
||||
|
|
|
@ -2,8 +2,8 @@ import { Response } from '@/lib/api/response';
|
|||
import { Tag } from '@/lib/db/models/tag';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { colorHash } from '@/lib/theme/color';
|
||||
import { ActionIcon, Button, ColorInput, Modal, Stack, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { hasLength, useForm } from '@mantine/form';
|
||||
import { ActionIcon, Button, ColorInput, Modal, Stack, TextInput, Tooltip } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconTag, IconTagOff, IconTextRecognition } from '@tabler/icons-react';
|
||||
import { useEffect } from 'react';
|
||||
|
@ -27,7 +27,7 @@ export default function EditTagModal({
|
|||
color: tag?.color || '',
|
||||
},
|
||||
validate: {
|
||||
name: hasLength({ min: 1 }, 'Name is required'),
|
||||
name: (value) => (value.length < 1 ? 'Name is required' : null),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -77,7 +77,7 @@ export default function EditTagModal({
|
|||
}, [tag]);
|
||||
|
||||
return (
|
||||
<Modal opened={open} onClose={onClose} title={<Title>Edit tag</Title>} zIndex={3000}>
|
||||
<Modal opened={open} onClose={onClose} title='Edit tag' zIndex={3000}>
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='sm'>
|
||||
<TextInput label='Name' placeholder='Enter a name...' {...form.getInputProps('name')} />
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import DashboardFile from '@/components/file/DashboardFile';
|
||||
import {
|
||||
Accordion,
|
||||
Button,
|
||||
|
@ -8,6 +7,7 @@ import {
|
|||
Pagination,
|
||||
Paper,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
|
@ -16,6 +16,11 @@ import Link from 'next/link';
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useApiPagination } from '../useApiPagination';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const DashboardFile = dynamic(() => import('@/components/file/DashboardFile'), {
|
||||
loading: () => <Skeleton height={350} animate />,
|
||||
});
|
||||
|
||||
export default function FavoriteFiles() {
|
||||
const router = useRouter();
|
||||
|
@ -53,58 +58,55 @@ export default function FavoriteFiles() {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Accordion variant='separated'>
|
||||
<Accordion.Item value='favorite'>
|
||||
<Accordion.Control>
|
||||
Favorite Files
|
||||
<Accordion.Panel>
|
||||
<SimpleGrid
|
||||
my='sm'
|
||||
cols={{
|
||||
base: 1,
|
||||
md: 2,
|
||||
lg: (data?.page.length ?? 0 > 0) ? 3 : 1,
|
||||
}}
|
||||
spacing='md'
|
||||
pos='relative'
|
||||
>
|
||||
{isLoading ? (
|
||||
<Paper withBorder h={200}>
|
||||
<LoadingOverlay visible />
|
||||
</Paper>
|
||||
) : (data?.page.length ?? 0 > 0) ? (
|
||||
data?.page.map((file) => <DashboardFile key={file.id} file={file} />)
|
||||
) : (
|
||||
<Paper withBorder p='sm'>
|
||||
<Center>
|
||||
<Stack>
|
||||
<Group>
|
||||
<IconFilesOff size='2rem' />
|
||||
<Title order={2}>No files found</Title>
|
||||
</Group>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='compact-sm'
|
||||
leftSection={<IconFileUpload size='1rem' />}
|
||||
component={Link}
|
||||
href='/dashboard/upload/file'
|
||||
>
|
||||
Upload a file
|
||||
</Button>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Paper>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
<Accordion variant='separated' my='xs'>
|
||||
<Accordion.Item value='favorite'>
|
||||
<Accordion.Control>Favorite Files</Accordion.Control>
|
||||
|
||||
<Center>
|
||||
<Pagination my='sm' value={page} onChange={setPage} total={data?.pages ?? 1} />
|
||||
</Center>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Control>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
</>
|
||||
<Accordion.Panel>
|
||||
<SimpleGrid
|
||||
my='sm'
|
||||
cols={{
|
||||
base: 1,
|
||||
md: 2,
|
||||
lg: (data?.page.length ?? 0 > 0) ? 3 : 1,
|
||||
}}
|
||||
spacing='md'
|
||||
pos='relative'
|
||||
>
|
||||
{isLoading ? (
|
||||
<Paper withBorder h={200}>
|
||||
<LoadingOverlay visible />
|
||||
</Paper>
|
||||
) : (data?.page.length ?? 0 > 0) ? (
|
||||
data?.page.map((file) => <DashboardFile key={file.id} file={file} />)
|
||||
) : (
|
||||
<Paper withBorder p='sm'>
|
||||
<Center>
|
||||
<Stack>
|
||||
<Group>
|
||||
<IconFilesOff size='2rem' />
|
||||
<Title order={2}>No files found</Title>
|
||||
</Group>
|
||||
<Button
|
||||
variant='outline'
|
||||
size='compact-sm'
|
||||
leftSection={<IconFileUpload size='1rem' />}
|
||||
component={Link}
|
||||
href='/dashboard/upload/file'
|
||||
>
|
||||
Upload a file
|
||||
</Button>
|
||||
</Stack>
|
||||
</Center>
|
||||
</Paper>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
|
||||
<Center>
|
||||
<Pagination my='sm' value={page} onChange={setPage} total={data?.pages ?? 1} />
|
||||
</Center>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ import { Folder } from '@/lib/db/models/folder';
|
|||
import TagPill from '../tags/TagPill';
|
||||
import { Tag } from '@/lib/db/models/tag';
|
||||
import Link from 'next/link';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
type ReducerQuery = {
|
||||
state: { name: string; originalName: string; type: string; tags: string };
|
||||
|
@ -173,10 +174,9 @@ function TagsFilter({
|
|||
export default function FileTable({ id }: { id?: string }) {
|
||||
const router = useRouter();
|
||||
const clipboard = useClipboard();
|
||||
const [searchThreshold, warnDeletion] = useSettingsStore((state) => [
|
||||
state.settings.searchThreshold,
|
||||
state.settings.warnDeletion,
|
||||
]);
|
||||
const [searchThreshold, warnDeletion] = useSettingsStore(
|
||||
useShallow((state) => [state.settings.searchThreshold, state.settings.warnDeletion]),
|
||||
);
|
||||
|
||||
const { data: folders } = useSWR<Extract<Response['/api/user/folders'], Folder[]>>(
|
||||
'/api/user/folders?noincl=true',
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import DashboardFile from '@/components/file/DashboardFile';
|
||||
import { Button, Center, Group, Pagination, Paper, SimpleGrid, Skeleton, Stack, Title } from '@mantine/core';
|
||||
import { IconFileUpload, IconFilesOff } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useApiPagination } from '../useApiPagination';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const DashboardFile = dynamic(() => import('@/components/file/DashboardFile'), {
|
||||
loading: () => <Skeleton height={350} animate />,
|
||||
});
|
||||
|
||||
export default function Files({ id }: { id?: string }) {
|
||||
const router = useRouter();
|
||||
|
@ -35,7 +39,7 @@ export default function Files({ id }: { id?: string }) {
|
|||
cols={{
|
||||
base: 1,
|
||||
md: 2,
|
||||
lg: (data?.page.length ?? 0 > 0) ? 3 : 1,
|
||||
lg: (data?.page.length ?? 0 > 0) || isLoading ? 3 : 1,
|
||||
}}
|
||||
spacing='md'
|
||||
pos='relative'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import DashboardFile from '@/components/file/DashboardFile';
|
||||
import { Folder } from '@/lib/db/models/folder';
|
||||
import { Modal, Paper, SimpleGrid, Title } from '@mantine/core';
|
||||
import { Modal, Paper, SimpleGrid } from '@mantine/core';
|
||||
|
||||
export default function ViewFilesModal({
|
||||
folder,
|
||||
|
@ -16,7 +16,7 @@ export default function ViewFilesModal({
|
|||
size='auto'
|
||||
zIndex={100}
|
||||
centered
|
||||
title={<Title>{folder?.name}</Title>}
|
||||
title={`Files in ${folder?.name}`}
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Response } from '@/lib/api/response';
|
||||
import { Folder } from '@/lib/db/models/folder';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { Anchor, Title } from '@mantine/core';
|
||||
import { Anchor } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
|
@ -12,7 +12,7 @@ import { mutate } from 'swr';
|
|||
export async function deleteFolder(folder: Folder) {
|
||||
modals.openConfirmModal({
|
||||
centered: true,
|
||||
title: <Title>Delete {folder.name}?</Title>,
|
||||
title: `Delete ${folder.name}?`,
|
||||
children: `Are you sure you want to delete ${folder.name}? This action cannot be undone.`,
|
||||
labels: {
|
||||
cancel: 'Cancel',
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Folder } from '@/lib/db/models/folder';
|
|||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { useViewStore } from '@/lib/store/view';
|
||||
import { ActionIcon, Button, Group, Modal, Stack, Switch, TextInput, Title, Tooltip } from '@mantine/core';
|
||||
import { hasLength, useForm } from '@mantine/form';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconFolderPlus, IconPlus } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
|
@ -23,7 +23,7 @@ export default function DashboardFolders() {
|
|||
isPublic: false,
|
||||
},
|
||||
validate: {
|
||||
name: hasLength({ min: 1 }, 'Name is required'),
|
||||
name: (value) => (value.length < 1 ? 'Name is required' : null),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -51,7 +51,7 @@ export default function DashboardFolders() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Modal centered opened={open} onClose={() => setOpen(false)} title={<Title>Create a folder</Title>}>
|
||||
<Modal centered opened={open} onClose={() => setOpen(false)} title='Create a folder'>
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='sm'>
|
||||
<TextInput label='Name' placeholder='Enter a name...' {...form.getInputProps('name')} />
|
||||
|
|
|
@ -60,7 +60,7 @@ export default function DashboardInvites() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Modal centered opened={open} onClose={() => setOpen(false)} title={<Title>Create an invite</Title>}>
|
||||
<Modal centered opened={open} onClose={() => setOpen(false)} title='Create an invite'>
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='sm'>
|
||||
<Select
|
||||
|
|
|
@ -11,7 +11,7 @@ export default function ExternalAuthButton({
|
|||
}: {
|
||||
provider: string;
|
||||
alpha: number;
|
||||
leftSection: JSX.Element;
|
||||
leftSection: React.ReactNode;
|
||||
}) {
|
||||
const theme = useMantineTheme();
|
||||
const colorHover = darken(`${provider.toLowerCase()}.0`, alpha, theme);
|
||||
|
|
|
@ -29,7 +29,7 @@ export default function DashboardMetrics() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Modal title={<Title>Change Range</Title>} opened={open} onClose={() => setOpen(false)} size='auto'>
|
||||
<Modal title='Change range' opened={open} onClose={() => setOpen(false)} size='auto'>
|
||||
<Paper withBorder>
|
||||
<DatePicker
|
||||
type='range'
|
||||
|
|
|
@ -1,39 +1,56 @@
|
|||
import { Metric } from '@/lib/db/models/metric';
|
||||
import { ChartTooltip, LineChart } from '@mantine/charts';
|
||||
import { Paper, Title } from '@mantine/core';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const Line = dynamic(() => import('@ant-design/plots').then(({ Line }) => Line), { ssr: false });
|
||||
|
||||
export default function FilesUrlsCountGraph({ metrics }: { metrics: Metric[] }) {
|
||||
const sortedMetrics = metrics.sort(
|
||||
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
|
||||
);
|
||||
|
||||
return (
|
||||
<Paper radius='sm' withBorder p='sm'>
|
||||
<Title order={3}>Count</Title>
|
||||
|
||||
<Line
|
||||
data={[
|
||||
...metrics.map((metric) => ({
|
||||
date: metric.createdAt,
|
||||
sum: metric.data.files,
|
||||
type: 'Files',
|
||||
})),
|
||||
...metrics.map((metric) => ({
|
||||
date: metric.createdAt,
|
||||
sum: metric.data.urls,
|
||||
type: 'URLs',
|
||||
})),
|
||||
<LineChart
|
||||
mt='xs'
|
||||
h={400}
|
||||
data={sortedMetrics.map((metric) => ({
|
||||
date: new Date(metric.createdAt).getTime(),
|
||||
files: metric.data.files,
|
||||
urls: metric.data.urls,
|
||||
}))}
|
||||
series={[
|
||||
{
|
||||
name: 'files',
|
||||
label: 'Files',
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
name: 'urls',
|
||||
label: 'URLs',
|
||||
color: 'green',
|
||||
},
|
||||
]}
|
||||
xField='date'
|
||||
yField='sum'
|
||||
seriesField='type'
|
||||
xAxis={{
|
||||
type: 'time',
|
||||
mask: 'YYYY-MM-DD HH:mm:ss',
|
||||
dataKey='date'
|
||||
curveType='natural'
|
||||
lineChartProps={{ syncId: 'datedStatistics' }}
|
||||
xAxisProps={{
|
||||
tickFormatter: (v) => new Date(v).toLocaleString(),
|
||||
}}
|
||||
legend={{
|
||||
position: 'top',
|
||||
tooltipProps={{
|
||||
content: ({ label, payload }) => (
|
||||
<ChartTooltip
|
||||
label={new Date(label).toLocaleString()}
|
||||
payload={payload}
|
||||
series={[
|
||||
{ name: 'files', label: 'Files' },
|
||||
{ name: 'urls', label: 'URLs' },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
padding='auto'
|
||||
smooth
|
||||
connectNulls
|
||||
withDots={false}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
|
|
|
@ -29,20 +29,12 @@ export function StatsTablesSkeleton() {
|
|||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{[...Array(5)].map((i) => (
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Table.Tr key={i}>
|
||||
<Table.Td>
|
||||
<SkeletonText />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<SkeletonText />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<SkeletonText />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<SkeletonText />
|
||||
</Table.Td>
|
||||
<SkeletonText />
|
||||
<SkeletonText />
|
||||
<SkeletonText />
|
||||
<SkeletonText />
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
|
@ -61,17 +53,11 @@ export function StatsTablesSkeleton() {
|
|||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{[...Array(5)].map((i) => (
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Table.Tr key={i}>
|
||||
<Table.Td>
|
||||
<SkeletonText />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<SkeletonText />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<SkeletonText />
|
||||
</Table.Td>
|
||||
<SkeletonText />
|
||||
<SkeletonText />
|
||||
<SkeletonText />
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
|
@ -89,14 +75,10 @@ export function StatsTablesSkeleton() {
|
|||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{[...Array(5)].map((i) => (
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Table.Tr key={i}>
|
||||
<Table.Td>
|
||||
<SkeletonText />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<SkeletonText />
|
||||
</Table.Td>
|
||||
<SkeletonText />
|
||||
<SkeletonText />
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
|
|
|
@ -1,37 +1,51 @@
|
|||
import { bytes } from '@/lib/bytes';
|
||||
import { Metric } from '@/lib/db/models/metric';
|
||||
import { LineChart, ChartTooltip } from '@mantine/charts';
|
||||
import { Paper, Title } from '@mantine/core';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const Line = dynamic(() => import('@ant-design/plots').then(({ Line }) => Line), { ssr: false });
|
||||
|
||||
export default function StorageGraph({ metrics }: { metrics: Metric[] }) {
|
||||
const sortedMetrics = metrics.sort(
|
||||
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
|
||||
);
|
||||
|
||||
return (
|
||||
<Paper radius='sm' withBorder p='sm' mt='md'>
|
||||
<Title order={3} mb='sm'>
|
||||
Storage Used
|
||||
</Title>
|
||||
|
||||
<Line
|
||||
data={metrics.map((metric) => ({
|
||||
date: metric.createdAt,
|
||||
<LineChart
|
||||
mt='xs'
|
||||
h={400}
|
||||
data={sortedMetrics.map((metric) => ({
|
||||
date: new Date(metric.createdAt).getTime(),
|
||||
storage: metric.data.storage,
|
||||
}))}
|
||||
xField='date'
|
||||
yField='storage'
|
||||
xAxis={{
|
||||
type: 'time',
|
||||
mask: 'YYYY-MM-DD HH:mm:ss',
|
||||
}}
|
||||
yAxis={{
|
||||
label: {
|
||||
formatter: (v) => bytes(Number(v)),
|
||||
series={[
|
||||
{
|
||||
name: 'storage',
|
||||
label: 'Storage Used',
|
||||
},
|
||||
]}
|
||||
dataKey='date'
|
||||
curveType='natural'
|
||||
valueFormatter={(v) => bytes(Number(v))}
|
||||
lineChartProps={{ syncId: 'datedStatistics' }}
|
||||
xAxisProps={{
|
||||
tickFormatter: (v) => new Date(v).toLocaleString(),
|
||||
}}
|
||||
tooltip={{
|
||||
formatter: (v) => ({ name: 'Storage Used', value: bytes(Number(v.storage)) }),
|
||||
tooltipProps={{
|
||||
content: ({ label, payload }) => (
|
||||
<ChartTooltip
|
||||
label={new Date(label).toLocaleString()}
|
||||
payload={payload}
|
||||
valueFormatter={(v) => bytes(Number(v))}
|
||||
series={[{ name: 'storage', label: 'Storage Used' }]}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
smooth
|
||||
connectNulls
|
||||
withDots={false}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
|
|
|
@ -1,21 +1,27 @@
|
|||
import { Metric } from '@/lib/db/models/metric';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const Pie = dynamic(() => import('@ant-design/plots').then(({ Pie }) => Pie), { ssr: false });
|
||||
import { colorHash } from '@/lib/theme/color';
|
||||
import { PieChart } from '@mantine/charts';
|
||||
|
||||
export default function TypesPieChart({ metric }: { metric: Metric }) {
|
||||
return (
|
||||
<Pie
|
||||
data={metric.data.types}
|
||||
angleField='sum'
|
||||
colorField='type'
|
||||
radius={0.8}
|
||||
label={{
|
||||
type: 'outer',
|
||||
content: '{name} - {percentage}',
|
||||
<PieChart
|
||||
data={metric.data.types.map((type) => ({
|
||||
name: type.type,
|
||||
value: type.sum,
|
||||
color: colorHash(type.type),
|
||||
}))}
|
||||
withLabels
|
||||
labelsPosition='outside'
|
||||
labelsType='value'
|
||||
withTooltip
|
||||
tooltipDataSource='segment'
|
||||
pieProps={{
|
||||
label: ({ name }) => name,
|
||||
}}
|
||||
legend={false}
|
||||
interactions={[{ type: 'pie-legend-active' }, { type: 'element-active' }]}
|
||||
w='100%'
|
||||
size={200}
|
||||
/>
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import { Metric } from '@/lib/db/models/metric';
|
||||
import { ChartTooltip, LineChart } from '@mantine/charts';
|
||||
import { Paper, Title } from '@mantine/core';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const Line = dynamic(() => import('@ant-design/plots').then(({ Line }) => Line), { ssr: false });
|
||||
|
||||
export default function ViewsGraph({ metrics }: { metrics: Metric[] }) {
|
||||
const sortedMetrics = metrics.sort(
|
||||
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
|
||||
);
|
||||
|
||||
return (
|
||||
<Paper radius='sm' withBorder p='sm'>
|
||||
<Title order={3}>Views</Title>
|
||||
|
||||
<Line
|
||||
{/* <Line
|
||||
data={[
|
||||
...metrics.map((metric) => ({
|
||||
date: metric.createdAt,
|
||||
|
@ -39,6 +40,47 @@ export default function ViewsGraph({ metrics }: { metrics: Metric[] }) {
|
|||
}}
|
||||
padding='auto'
|
||||
smooth
|
||||
/> */}
|
||||
<LineChart
|
||||
mt='xs'
|
||||
h={400}
|
||||
data={sortedMetrics.map((metric) => ({
|
||||
date: new Date(metric.createdAt).getTime(),
|
||||
files: metric.data.fileViews,
|
||||
urls: metric.data.urlViews,
|
||||
}))}
|
||||
series={[
|
||||
{
|
||||
name: 'files',
|
||||
label: 'File Views',
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
name: 'urls',
|
||||
label: 'URL Views',
|
||||
color: 'green',
|
||||
},
|
||||
]}
|
||||
dataKey='date'
|
||||
curveType='natural'
|
||||
lineChartProps={{ syncId: 'datedStatistics' }}
|
||||
xAxisProps={{
|
||||
tickFormatter: (v) => new Date(v).toLocaleString(),
|
||||
}}
|
||||
tooltipProps={{
|
||||
content: ({ label, payload }) => (
|
||||
<ChartTooltip
|
||||
label={new Date(label).toLocaleString()}
|
||||
payload={payload}
|
||||
series={[
|
||||
{ name: 'files', label: 'File Views' },
|
||||
{ name: 'urls', label: 'URL Views' },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
connectNulls
|
||||
withDots={false}
|
||||
/>
|
||||
</Paper>
|
||||
);
|
||||
|
|
|
@ -25,9 +25,6 @@ const fetcher = async ([url, options]: [string, ApiStatsOptions]) => {
|
|||
};
|
||||
|
||||
export function useApiStats(options: ApiStatsOptions = {}) {
|
||||
if (!options.from && !options.to)
|
||||
return { data: undefined, error: undefined, isLoading: false, mutate: () => {} };
|
||||
|
||||
const { data, error, isLoading, mutate } = useSWR<Response['/api/stats']>(['/api/stats', options], {
|
||||
fetcher,
|
||||
});
|
||||
|
|
|
@ -1,20 +1,54 @@
|
|||
import { Response } from '@/lib/api/response';
|
||||
import { Group, SimpleGrid, Stack, Title } from '@mantine/core';
|
||||
import { Group, SimpleGrid, Skeleton, Stack, Title } from '@mantine/core';
|
||||
import useSWR from 'swr';
|
||||
import ServerSettingsChunks from './parts/ServerSettingsChunks';
|
||||
import ServerSettingsCore from './parts/ServerSettingsCore';
|
||||
import ServerSettingsDiscord from './parts/ServerSettingsDiscord';
|
||||
import ServerSettingsFeatures from './parts/ServerSettingsFeatures';
|
||||
import ServerSettingsFiles from './parts/ServerSettingsFiles';
|
||||
import ServerSettingsHttpWebhook from './parts/ServerSettingsHttpWebhook';
|
||||
import ServerSettingsInvites from './parts/ServerSettingsInvites';
|
||||
import ServerSettingsMfa from './parts/ServerSettingsMfa';
|
||||
import ServerSettingsOauth from './parts/ServerSettingsOauth';
|
||||
import ServerSettingsRatelimit from './parts/ServerSettingsRatelimit';
|
||||
import ServerSettingsTasks from './parts/ServerSettingsTasks';
|
||||
import ServerSettingsUrls from './parts/ServerSettingsUrls';
|
||||
import ServerSettingsWebsite from './parts/ServerSettingsWebsite';
|
||||
import ServerSettingsPWA from './parts/ServerSettingsPWA';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
function SettingsSkeleton() {
|
||||
return <Skeleton height={280} animate />;
|
||||
}
|
||||
|
||||
const ServerSettingsCore = dynamic(() => import('./parts/ServerSettingsCore'), {
|
||||
loading: () => <SettingsSkeleton />,
|
||||
});
|
||||
const ServerSettingsChunks = dynamic(() => import('./parts/ServerSettingsChunks'), {
|
||||
loading: () => <SettingsSkeleton />,
|
||||
});
|
||||
const ServerSettingsDiscord = dynamic(() => import('./parts/ServerSettingsDiscord'), {
|
||||
loading: () => <SettingsSkeleton />,
|
||||
});
|
||||
const ServerSettingsFeatures = dynamic(() => import('./parts/ServerSettingsFeatures'), {
|
||||
loading: () => <SettingsSkeleton />,
|
||||
});
|
||||
const ServerSettingsFiles = dynamic(() => import('./parts/ServerSettingsFiles'), {
|
||||
loading: () => <SettingsSkeleton />,
|
||||
});
|
||||
const ServerSettingsHttpWebhook = dynamic(() => import('./parts/ServerSettingsHttpWebhook'), {
|
||||
loading: () => <SettingsSkeleton />,
|
||||
});
|
||||
const ServerSettingsInvites = dynamic(() => import('./parts/ServerSettingsInvites'), {
|
||||
loading: () => <SettingsSkeleton />,
|
||||
});
|
||||
const ServerSettingsMfa = dynamic(() => import('./parts/ServerSettingsMfa'), {
|
||||
loading: () => <SettingsSkeleton />,
|
||||
});
|
||||
const ServerSettingsOauth = dynamic(() => import('./parts/ServerSettingsOauth'), {
|
||||
loading: () => <SettingsSkeleton />,
|
||||
});
|
||||
const ServerSettingsRatelimit = dynamic(() => import('./parts/ServerSettingsRatelimit'), {
|
||||
loading: () => <SettingsSkeleton />,
|
||||
});
|
||||
const ServerSettingsTasks = dynamic(() => import('./parts/ServerSettingsTasks'), {
|
||||
loading: () => <SettingsSkeleton />,
|
||||
});
|
||||
const ServerSettingsUrls = dynamic(() => import('./parts/ServerSettingsUrls'), {
|
||||
loading: () => <SettingsSkeleton />,
|
||||
});
|
||||
const ServerSettingsWebsite = dynamic(() => import('./parts/ServerSettingsWebsite'), {
|
||||
loading: () => <SettingsSkeleton />,
|
||||
});
|
||||
const ServerSettingsPWA = dynamic(() => import('./parts/ServerSettingsPWA'), {
|
||||
loading: () => <SettingsSkeleton />,
|
||||
});
|
||||
|
||||
export default function DashboardSettings() {
|
||||
const { data, isLoading, error } = useSWR<Response['/api/server/settings']>('/api/server/settings');
|
||||
|
|
|
@ -19,7 +19,7 @@ export default function ServerSettingsCore({
|
|||
}>({
|
||||
initialValues: {
|
||||
coreReturnHttpsUrls: false,
|
||||
coreDefaultDomain: null,
|
||||
coreDefaultDomain: '',
|
||||
coreTempDirectory: '/tmp/zipline',
|
||||
},
|
||||
});
|
||||
|
@ -37,7 +37,7 @@ export default function ServerSettingsCore({
|
|||
useEffect(() => {
|
||||
form.setValues({
|
||||
coreReturnHttpsUrls: data?.coreReturnHttpsUrls ?? false,
|
||||
coreDefaultDomain: data?.coreDefaultDomain ?? null,
|
||||
coreDefaultDomain: data?.coreDefaultDomain ?? '',
|
||||
coreTempDirectory: data?.coreTempDirectory ?? '/tmp/zipline',
|
||||
});
|
||||
}, [data]);
|
||||
|
|
|
@ -41,7 +41,7 @@ export default function ServerSettingsFiles({
|
|||
filesDefaultFormat: 'random',
|
||||
filesDisabledExtensions: '',
|
||||
filesMaxFileSize: '100mb',
|
||||
filesDefaultExpiration: null,
|
||||
filesDefaultExpiration: '',
|
||||
filesAssumeMimetypes: false,
|
||||
filesDefaultDateFormat: 'YYYY-MM-DD_HH:mm:ss',
|
||||
filesRemoveGpsMetadata: false,
|
||||
|
@ -84,7 +84,7 @@ export default function ServerSettingsFiles({
|
|||
filesDefaultFormat: data?.filesDefaultFormat ?? 'random',
|
||||
filesDisabledExtensions: data?.filesDisabledExtensions.join(', ') ?? '',
|
||||
filesMaxFileSize: bytes(data?.filesMaxFileSize ?? 104857600),
|
||||
filesDefaultExpiration: data?.filesDefaultExpiration ? ms(data.filesDefaultExpiration) : null,
|
||||
filesDefaultExpiration: data?.filesDefaultExpiration ? ms(data.filesDefaultExpiration) : '',
|
||||
filesAssumeMimetypes: data?.filesAssumeMimetypes ?? false,
|
||||
filesDefaultDateFormat: data?.filesDefaultDateFormat ?? 'YYYY-MM-DD_HH:mm:ss',
|
||||
filesRemoveGpsMetadata: data?.filesRemoveGpsMetadata ?? false,
|
||||
|
|
|
@ -48,7 +48,7 @@ export default function ServerSettingsWebsite({
|
|||
// @ts-ignore
|
||||
try {
|
||||
sendValues.websiteExternalLinks = JSON.parse(values.websiteExternalLinks);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
form.setFieldError('websiteExternalLinks', 'Invalid JSON');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { useThemes } from '@/components/ThemeProvider';
|
|||
import { useSettingsStore } from '@/lib/store/settings';
|
||||
import { Group, NumberInput, Paper, Select, Stack, Switch, Text, Title } from '@mantine/core';
|
||||
import { IconMoonFilled, IconPaintFilled, IconPercentage, IconSunFilled } from '@tabler/icons-react';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
const renderThemeOption =
|
||||
(themes: ReturnType<typeof useThemes>) =>
|
||||
|
@ -19,7 +20,7 @@ const renderThemeOption =
|
|||
);
|
||||
|
||||
export default function SettingsDashboard() {
|
||||
const [settings, update] = useSettingsStore((state) => [state.settings, state.update]);
|
||||
const [settings, update] = useSettingsStore(useShallow((state) => [state.settings, state.update]));
|
||||
const themes = useThemes();
|
||||
|
||||
const sortedThemes = themes.sort((a, b) => {
|
||||
|
|
|
@ -14,7 +14,7 @@ export default function SettingsExports() {
|
|||
|
||||
const handleNewExport = async () => {
|
||||
modals.openConfirmModal({
|
||||
title: <Title>New Export?</Title>,
|
||||
title: 'New export?',
|
||||
children:
|
||||
'Are you sure you want to start a new export? If you have a lot of files, this may take a while.',
|
||||
onConfirm: async () => {
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
IconFileX,
|
||||
} from '@tabler/icons-react';
|
||||
import { mutate } from 'swr';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
const alignIcons: Record<string, React.ReactNode> = {
|
||||
left: <IconAlignLeft size='1rem' />,
|
||||
|
@ -35,7 +36,7 @@ const alignIcons: Record<string, React.ReactNode> = {
|
|||
};
|
||||
|
||||
export default function SettingsFileView() {
|
||||
const [user, setUser] = useUserStore((state) => [state.user, state.setUser]);
|
||||
const [user, setUser] = useUserStore(useShallow((state) => [state.user, state.setUser]));
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
|
|
|
@ -12,7 +12,6 @@ import {
|
|||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { IconDownload, IconEyeFilled, IconGlobe, IconPercentage, IconWriting } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
|
@ -111,7 +110,7 @@ export default function GeneratorButton({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Modal opened={opened} onClose={() => setOpen(false)} title={<Title>Generate {name}</Title>}>
|
||||
<Modal opened={opened} onClose={() => setOpen(false)} title={`Generate ${name}`}>
|
||||
{desc && (
|
||||
<Text size='sm' c='dimmed'>
|
||||
{desc}
|
||||
|
|
|
@ -3,7 +3,7 @@ import { fetchApi } from '@/lib/fetchApi';
|
|||
import { registerWeb } from '@/lib/passkey';
|
||||
import { useUserStore } from '@/lib/store/user';
|
||||
import { RegistrationResponseJSON } from '@github/webauthn-json/dist/types/browser-ponyfill';
|
||||
import { ActionIcon, Button, Group, Modal, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
|
||||
import { ActionIcon, Button, Group, Modal, Paper, Stack, Text, TextInput } from '@mantine/core';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { UserPasskey } from '@prisma/client';
|
||||
|
@ -27,7 +27,7 @@ export default function PasskeyButton() {
|
|||
const res = await registerWeb(user!);
|
||||
setNamerShown(true);
|
||||
setSavedKey(res.toJSON());
|
||||
} catch (e) {
|
||||
} catch {
|
||||
setPasskeyErrored(true);
|
||||
setPasskeyLoading(false);
|
||||
setSavedKey(null);
|
||||
|
@ -73,7 +73,7 @@ export default function PasskeyButton() {
|
|||
|
||||
const removePasskey = async (passkey: UserPasskey) => {
|
||||
modals.openConfirmModal({
|
||||
title: <Title>Are you sure?</Title>,
|
||||
title: 'Are you sure?',
|
||||
children: `Your browser and device may still show "${passkey.name}" as an option to log in. If you want to remove it, you'll have to do so manually through your device's settings.`,
|
||||
labels: {
|
||||
confirm: `Remove "${passkey.name}"`,
|
||||
|
@ -118,11 +118,7 @@ export default function PasskeyButton() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
title={<Title>Manage passkeys</Title>}
|
||||
opened={passkeyOpen}
|
||||
onClose={() => setPasskeyOpen(false)}
|
||||
>
|
||||
<Modal title='Manage passkeys' opened={passkeyOpen} onClose={() => setPasskeyOpen(false)}>
|
||||
<Stack gap='sm'>
|
||||
<>
|
||||
{user?.passkeys?.map((passkey, i) => (
|
||||
|
|
|
@ -13,16 +13,16 @@ import {
|
|||
PinInput,
|
||||
Stack,
|
||||
Text,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconShieldLockFilled } from '@tabler/icons-react';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import useSWR, { mutate } from 'swr';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
export default function TwoFAButton() {
|
||||
const [user, setUser] = useUserStore((state) => [state.user, state.setUser]);
|
||||
const [user, setUser] = useUserStore(useShallow((state) => [state.user, state.setUser]));
|
||||
|
||||
const [totpOpen, setTotpOpen] = useState(false);
|
||||
const {
|
||||
|
@ -112,7 +112,7 @@ export default function TwoFAButton() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Modal title={<Title>Enable 2FA</Title>} opened={totpOpen} onClose={() => setTotpOpen(false)}>
|
||||
<Modal title='Enable 2FA' opened={totpOpen} onClose={() => setTotpOpen(false)}>
|
||||
<Stack gap='sm'>
|
||||
{user?.totpSecret ? (
|
||||
<Text size='sm' c='dimmed'>
|
||||
|
|
|
@ -37,8 +37,6 @@ const names = {
|
|||
function OAuthButton({ provider, linked }: { provider: OAuthProviderType; linked: boolean }) {
|
||||
const t = useMantineTheme();
|
||||
|
||||
console.log(t);
|
||||
|
||||
const unlink = async () => {
|
||||
const { error } = await fetchApi<Response['/api/auth/oauth']>('/api/auth/oauth', 'DELETE', {
|
||||
provider,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Response } from '@/lib/api/response';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { Button, Title } from '@mantine/core';
|
||||
import { Button } from '@mantine/core';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconTrashFilled } from '@tabler/icons-react';
|
||||
|
@ -8,7 +8,7 @@ import { IconTrashFilled } from '@tabler/icons-react';
|
|||
export default function ClearTempButton() {
|
||||
const openModal = () =>
|
||||
modals.openConfirmModal({
|
||||
title: <Title>Are you sure?</Title>,
|
||||
title: 'Are you sure?',
|
||||
children:
|
||||
'This will delete temporary files stored within the temporary directory (defined in the configuration). This should not cause harm unless there are files that are being processed still.',
|
||||
labels: { confirm: 'Yes, delete', cancel: 'Cancel' },
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Response } from '@/lib/api/response';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { Button, Title } from '@mantine/core';
|
||||
import { Button } from '@mantine/core';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconTrashFilled } from '@tabler/icons-react';
|
||||
|
@ -11,7 +11,7 @@ export default function ClearZerosButton() {
|
|||
|
||||
const openModal = () =>
|
||||
modals.openConfirmModal({
|
||||
title: <Title>Are you sure?</Title>,
|
||||
title: 'Are you sure?',
|
||||
children: `This will delete ${data?.files?.length ?? 0} files from the database and datasource.`,
|
||||
labels: { confirm: 'Yes, delete', cancel: 'Cancel' },
|
||||
confirmProps: { color: 'red' },
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Response } from '@/lib/api/response';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { Button, Group, Modal, Stack, Switch, Title } from '@mantine/core';
|
||||
import { Button, Group, Modal, Stack, Switch } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconVideo, IconVideoOff } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
|
@ -30,7 +30,7 @@ export default function GenThumbsButton() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Modal title={<Title>Are you sure?</Title>} opened={open} onClose={() => setOpen(false)}>
|
||||
<Modal title='Are you sure?' opened={open} onClose={() => setOpen(false)}>
|
||||
<Stack mb='md'>
|
||||
<span>
|
||||
This will generate thumbnails for all files that do not have a thumbnail set. Additionally you can
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { Response } from '@/lib/api/response';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { Export3, validateExport } from '@/lib/import/version3/validateExport';
|
||||
import { Alert, Button, Code, FileButton, Modal, Stack, Title } from '@mantine/core';
|
||||
import { Alert, Button, Code, FileButton, Modal, Stack } from '@mantine/core';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { showNotification, updateNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconCheck,
|
||||
|
@ -11,9 +14,6 @@ import {
|
|||
} from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Export3Details from './Export3Details';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { Response } from '@/lib/api/response';
|
||||
|
||||
export default function ImportButton() {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
@ -51,7 +51,7 @@ export default function ImportButton() {
|
|||
|
||||
const handleImport = async () => {
|
||||
modals.openConfirmModal({
|
||||
title: <Title>Are you sure?</Title>,
|
||||
title: 'Are you sure?',
|
||||
children:
|
||||
'This process will NOT overwrite existing data but will append to it. In case of conflicts, the imported data will be skipped and logged. If using a version 3 export, the entire importing process should be completed immediately after setting up Zipline.',
|
||||
labels: {
|
||||
|
@ -132,7 +132,7 @@ export default function ImportButton() {
|
|||
|
||||
if (Object.keys(data?.files ?? {}).length > 0) {
|
||||
modals.open({
|
||||
title: <Title>Next Steps</Title>,
|
||||
title: 'Are you sure?',
|
||||
children: (
|
||||
<>
|
||||
<p>
|
||||
|
@ -190,7 +190,7 @@ export default function ImportButton() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Modal opened={open} onClose={() => setOpen(false)} title={<Title>Import Data</Title>} size='xl'>
|
||||
<Modal opened={open} onClose={() => setOpen(false)} title='Import data' size='xl'>
|
||||
{export3 ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Response } from '@/lib/api/response';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { Button, Group, Modal, Stack, Switch, Title } from '@mantine/core';
|
||||
import { Button, Group, Modal, Stack, Switch } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconFileSearch } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
|
@ -34,7 +34,7 @@ export default function RequerySizeButton() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Modal title={<Title>Are you sure?</Title>} opened={open} onClose={() => setOpen(false)}>
|
||||
<Modal title='Are you sure?' opened={open} onClose={() => setOpen(false)}>
|
||||
<Stack mb='md'>
|
||||
<span>
|
||||
This will requery the size of every file stored within the database. Additionally you can use the
|
||||
|
|
|
@ -12,7 +12,7 @@ export default function SettingsSessions() {
|
|||
|
||||
const handleLogOutOfAllDevices = async () => {
|
||||
modals.openConfirmModal({
|
||||
title: <Title>Log out of all devices</Title>,
|
||||
title: 'Log out of all devices?',
|
||||
children:
|
||||
'Are you sure you want to log out of all devices? This will log you out of all devices except the current one.',
|
||||
onConfirm: async () => {
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { hasLength, useForm } from '@mantine/form';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import {
|
||||
IconAsteriskSimple,
|
||||
|
@ -26,9 +26,10 @@ import {
|
|||
} from '@tabler/icons-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { mutate } from 'swr';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
export default function SettingsUser() {
|
||||
const [user, setUser] = useUserStore((state) => [state.user, state.setUser]);
|
||||
const [user, setUser] = useUserStore(useShallow((state) => [state.user, state.setUser]));
|
||||
|
||||
const [tokenShown, setTokenShown] = useState(false);
|
||||
const [token, setToken] = useState('');
|
||||
|
@ -49,7 +50,7 @@ export default function SettingsUser() {
|
|||
password: '',
|
||||
},
|
||||
validate: {
|
||||
username: hasLength({ min: 1 }, 'Username is required'),
|
||||
username: (value) => (value.length < 1 ? 'Username is required' : null),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ import ToUploadFile from './ToUploadFile';
|
|||
import { bytes } from '@/lib/bytes';
|
||||
import { uploadPartialFiles } from '../uploadPartialFiles';
|
||||
import { humanizeDuration } from '@/lib/relativeTime';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
export default function UploadFile() {
|
||||
const theme = useMantineTheme();
|
||||
|
@ -34,11 +35,9 @@ export default function UploadFile() {
|
|||
const clipboard = useClipboard();
|
||||
const config = useConfig();
|
||||
|
||||
const [options, ephemeral, clearEphemeral] = useUploadOptionsStore((state) => [
|
||||
state.options,
|
||||
state.ephemeral,
|
||||
state.clearEphemeral,
|
||||
]);
|
||||
const [options, ephemeral, clearEphemeral] = useUploadOptionsStore(
|
||||
useShallow((state) => [state.options, state.ephemeral, state.clearEphemeral]),
|
||||
);
|
||||
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [progress, setProgress] = useState<{ percent: number; remaining: number; speed: number }>({
|
||||
|
|
|
@ -22,6 +22,7 @@ import { renderMode } from '../renderMode';
|
|||
import { uploadFiles } from '../uploadFiles';
|
||||
|
||||
import styles from './index.module.css';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
export default function UploadText({
|
||||
codeMeta,
|
||||
|
@ -30,11 +31,9 @@ export default function UploadText({
|
|||
}) {
|
||||
const clipboard = useClipboard();
|
||||
|
||||
const [options, ephemeral, clearEphemeral] = useUploadOptionsStore((state) => [
|
||||
state.options,
|
||||
state.ephemeral,
|
||||
state.clearEphemeral,
|
||||
]);
|
||||
const [options, ephemeral, clearEphemeral] = useUploadOptionsStore(
|
||||
useShallow((state) => [state.options, state.ephemeral, state.clearEphemeral]),
|
||||
);
|
||||
|
||||
const [selectedLanguage, setSelectedLanguage] = useState('txt');
|
||||
const [text, setText] = useState('');
|
||||
|
|
|
@ -16,7 +16,6 @@ import {
|
|||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
Title,
|
||||
useCombobox,
|
||||
} from '@mantine/core';
|
||||
import {
|
||||
|
@ -31,27 +30,28 @@ import {
|
|||
IconTrashFilled,
|
||||
IconWriting,
|
||||
} from '@tabler/icons-react';
|
||||
import ms from 'ms';
|
||||
import Link from 'next/link';
|
||||
import { useState } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import ms from 'ms';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
export default function UploadOptionsButton({ numFiles }: { numFiles: number }) {
|
||||
const config = useConfig();
|
||||
|
||||
const [opened, setOpen] = useState(false);
|
||||
const [options, ephemeral, setOption, setEphemeral, changes] = useUploadOptionsStore((state) => [
|
||||
state.options,
|
||||
state.ephemeral,
|
||||
state.setOption,
|
||||
state.setEphemeral,
|
||||
state.changes,
|
||||
]);
|
||||
|
||||
const [clearEphemeral, clearOptions] = useUploadOptionsStore((state) => [
|
||||
state.clearEphemeral,
|
||||
state.clearOptions,
|
||||
]);
|
||||
const [options, ephemeral, setOption, setEphemeral, changes, clearEphemeral, clearOptions] =
|
||||
useUploadOptionsStore(
|
||||
useShallow((state) => [
|
||||
state.options,
|
||||
state.ephemeral,
|
||||
state.setOption,
|
||||
state.setEphemeral,
|
||||
state.changes,
|
||||
state.clearEphemeral,
|
||||
state.clearOptions,
|
||||
]),
|
||||
);
|
||||
|
||||
const clearSettings = () => {
|
||||
clearEphemeral();
|
||||
|
@ -66,7 +66,7 @@ export default function UploadOptionsButton({ numFiles }: { numFiles: number })
|
|||
|
||||
return (
|
||||
<>
|
||||
<Modal centered opened={opened} onClose={() => setOpen(false)} title={<Title>Upload Options</Title>}>
|
||||
<Modal centered opened={opened} onClose={() => setOpen(false)} title='Upload Options'>
|
||||
<Text size='sm' c='dimmed'>
|
||||
These options will be applied to all files you upload and are saved in your browser.
|
||||
</Text>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Response } from '@/lib/api/response';
|
||||
import { ErrorBody } from '@/lib/response';
|
||||
import { UploadOptionsStore } from '@/lib/store/uploadOptions';
|
||||
import { ActionIcon, Anchor, Button, Group, Stack, Table, Title, Tooltip } from '@mantine/core';
|
||||
import { ActionIcon, Anchor, Button, Group, Stack, Table, Tooltip } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
|
@ -34,7 +34,7 @@ export function filesModal(
|
|||
};
|
||||
|
||||
modals.open({
|
||||
title: <Title>Uploaded Files</Title>,
|
||||
title: 'Uploaded files',
|
||||
size: 'auto',
|
||||
children: (
|
||||
<Table withTableBorder={false} withColumnBorders={false} highlightOnHover horizontalSpacing={'sm'}>
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Response } from '@/lib/api/response';
|
|||
import { randomCharacters } from '@/lib/random';
|
||||
import { ErrorBody } from '@/lib/response';
|
||||
import { UploadOptionsStore } from '@/lib/store/uploadOptions';
|
||||
import { ActionIcon, Anchor, Group, Stack, Table, Text, Title, Tooltip } from '@mantine/core';
|
||||
import { ActionIcon, Anchor, Group, Stack, Table, Text, Tooltip } from '@mantine/core';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { hideNotification, notifications } from '@mantine/notifications';
|
||||
|
@ -36,7 +36,7 @@ export function filesModal(
|
|||
};
|
||||
|
||||
modals.open({
|
||||
title: <Title>Uploaded Files</Title>,
|
||||
title: 'Uploaded files',
|
||||
size: 'auto',
|
||||
children: (
|
||||
<Table withTableBorder={false} withColumnBorders={false} highlightOnHover horizontalSpacing={'sm'}>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Url } from '@/lib/db/models/url';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { Button, Divider, Modal, NumberInput, PasswordInput, Stack, TextInput, Title } from '@mantine/core';
|
||||
import { Button, Divider, Modal, NumberInput, PasswordInput, Stack, TextInput } from '@mantine/core';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { IconEye, IconKey, IconPencil, IconPencilOff, IconTrashFilled } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
|
@ -87,11 +87,7 @@ export default function EditUrlModal({
|
|||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<Title>Editing "{url.vanity ?? url.code}"</Title>}
|
||||
opened={open}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Modal title={`Editing "${url.vanity ?? url.code}"`} opened={open} onClose={onClose}>
|
||||
<Stack gap='xs' my='sm'>
|
||||
<NumberInput
|
||||
label='Max Views'
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { hasLength, useForm } from '@mantine/form';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useClipboard } from '@mantine/hooks';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
|
@ -45,7 +45,7 @@ export default function DashboardURLs() {
|
|||
password: '',
|
||||
},
|
||||
validate: {
|
||||
url: hasLength({ min: 1 }, 'URL is required'),
|
||||
url: (value) => (value.length < 1 ? 'URL is required' : null),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -91,7 +91,7 @@ export default function DashboardURLs() {
|
|||
};
|
||||
|
||||
modals.open({
|
||||
title: <Title>Shortened URL</Title>,
|
||||
title: 'Shortened URL',
|
||||
size: 'auto',
|
||||
children: (
|
||||
<Group justify='space-between'>
|
||||
|
@ -123,7 +123,7 @@ export default function DashboardURLs() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Modal centered opened={open} onClose={() => setOpen(false)} title={<Title>Shorten a URL</Title>}>
|
||||
<Modal centered opened={open} onClose={() => setOpen(false)} title='Shorten URL'>
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='sm'>
|
||||
<TextInput label='URL' placeholder='https://example.com' {...form.getInputProps('url')} />
|
||||
|
|
|
@ -12,6 +12,7 @@ import { useClipboard } from '@mantine/hooks';
|
|||
import { useSettingsStore } from '@/lib/store/settings';
|
||||
import { formatRootUrl } from '@/lib/url';
|
||||
import EditUrlModal from '../EditUrlModal';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
const NAMES = {
|
||||
code: 'Code',
|
||||
|
@ -104,10 +105,9 @@ export default function UrlTableView() {
|
|||
destination: '',
|
||||
},
|
||||
);
|
||||
const [warnDeletion, searchThreshold] = useSettingsStore((s) => [
|
||||
s.settings.warnDeletion,
|
||||
s.settings.searchThreshold,
|
||||
]);
|
||||
const [warnDeletion, searchThreshold] = useSettingsStore(
|
||||
useShallow((state) => [state.settings.warnDeletion, state.settings.searchThreshold]),
|
||||
);
|
||||
|
||||
const { data, isLoading } = useSWR<Extract<Response['/api/user/urls'], Url[]>>(
|
||||
{
|
||||
|
|
|
@ -154,7 +154,7 @@ export default function EditUserModal({
|
|||
}, [user]);
|
||||
|
||||
return (
|
||||
<Modal centered title={<Title>Edit {user?.username ?? ''}</Title>} onClose={onClose} opened={opened}>
|
||||
<Modal centered title={`Edit ${user?.username ?? ''}`} onClose={onClose} opened={opened}>
|
||||
<Text size='sm' c='dimmed'>
|
||||
Any fields that are blank will be omitted, and will not be updated.
|
||||
</Text>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Response } from '@/lib/api/response';
|
||||
import { User } from '@/lib/db/models/user';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { Title } from '@mantine/core';
|
||||
import { modals } from '@mantine/modals';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconUserCancel, IconUserMinus } from '@tabler/icons-react';
|
||||
|
@ -10,7 +9,7 @@ import { mutate } from 'swr';
|
|||
export async function deleteUser(user: User) {
|
||||
modals.openConfirmModal({
|
||||
centered: true,
|
||||
title: <Title>Delete {user.username}?</Title>,
|
||||
title: `Delete ${user.username}?`,
|
||||
children: `Are you sure you want to delete ${user.username}? This action cannot be undone.`,
|
||||
labels: {
|
||||
cancel: 'Cancel',
|
||||
|
@ -19,7 +18,7 @@ export async function deleteUser(user: User) {
|
|||
onConfirm: () =>
|
||||
modals.openConfirmModal({
|
||||
centered: true,
|
||||
title: <Title>Delete {user.username}'s data?</Title>,
|
||||
title: `Delete ${user.username}'s data?`,
|
||||
children: `Would you like to delete ${user.username}'s files and urls? This action cannot be undone.`,
|
||||
labels: {
|
||||
cancel: 'No, keep everything & only delete user',
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import GridTableSwitcher from '@/components/GridTableSwitcher';
|
||||
import { Response } from '@/lib/api/response';
|
||||
import { readToDataURL } from '@/lib/base64';
|
||||
import { User } from '@/lib/db/models/user';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { canInteract } from '@/lib/role';
|
||||
import { useUserStore } from '@/lib/store/user';
|
||||
import { useViewStore } from '@/lib/store/view';
|
||||
import {
|
||||
ActionIcon,
|
||||
|
@ -16,16 +19,13 @@ import {
|
|||
Title,
|
||||
Tooltip,
|
||||
} from '@mantine/core';
|
||||
import { hasLength, useForm } from '@mantine/form';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconPhotoMinus, IconUserCancel, IconUserPlus } from '@tabler/icons-react';
|
||||
import { useState } from 'react';
|
||||
import { mutate } from 'swr';
|
||||
import UserGridView from './views/UserGridView';
|
||||
import UserTableView from './views/UserTableView';
|
||||
import { readToDataURL } from '@/lib/base64';
|
||||
import { canInteract } from '@/lib/role';
|
||||
import { useUserStore } from '@/lib/store/user';
|
||||
|
||||
export default function DashboardUsers() {
|
||||
const currentUser = useUserStore((state) => state.user);
|
||||
|
@ -45,8 +45,8 @@ export default function DashboardUsers() {
|
|||
avatar: null,
|
||||
},
|
||||
validate: {
|
||||
username: hasLength({ min: 1 }, 'Username is required'),
|
||||
password: hasLength({ min: 1 }, 'Password is required'),
|
||||
username: (value) => (value.length < 1 ? 'Username is required' : null),
|
||||
password: (value) => (value.length < 1 ? 'Password is required' : null),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -95,7 +95,7 @@ export default function DashboardUsers() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Modal centered opened={open} onClose={() => setOpen(false)} title={<Title>Create a new user</Title>}>
|
||||
<Modal centered opened={open} onClose={() => setOpen(false)} title='Create a new user'>
|
||||
<form onSubmit={form.onSubmit(onSubmit)}>
|
||||
<Stack gap='sm'>
|
||||
<TextInput
|
||||
|
|
|
@ -3,6 +3,6 @@ import bytesFn, { BytesOptions } from 'bytes';
|
|||
export function bytes(value: string): number;
|
||||
export function bytes(value: number, options?: BytesOptions): string;
|
||||
export function bytes(value: string | number, options?: BytesOptions): string | number {
|
||||
if (typeof value === 'string') return bytesFn(value);
|
||||
return bytesFn(Number(value), { ...options, unitSeparator: ' ' });
|
||||
if (typeof value === 'string') return bytesFn(value) ?? 0;
|
||||
return bytesFn(Number(value), { ...options, unitSeparator: ' ' }) ?? '';
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ZodError, ZodIssue, z } from 'zod';
|
||||
import { type ZodIssue, z } from 'zod';
|
||||
import { PROP_TO_ENV, ParsedConfig } from './read';
|
||||
import { log } from '../logger';
|
||||
import { join, resolve } from 'path';
|
||||
|
@ -328,30 +328,20 @@ export function validateConfigObject(env: ParsedConfig): Config {
|
|||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const validated = schema.parse(env);
|
||||
const validated = schema.safeParse(env);
|
||||
if (!validated.success) {
|
||||
logger.error('There was an error while validating the environment.');
|
||||
|
||||
if (!validated) {
|
||||
logger.error('There was an error while validating the environment.');
|
||||
process.exit(1);
|
||||
for (const error of validated.error.errors) {
|
||||
handleError(error);
|
||||
}
|
||||
|
||||
logger.debug('reloaded config');
|
||||
|
||||
return validated;
|
||||
} catch (e) {
|
||||
if (e instanceof ZodError) {
|
||||
logger.error(`There were ${e.errors.length} error(s) while validating the environment.`);
|
||||
|
||||
for (const error of e.errors) {
|
||||
handleError(error);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
throw e;
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
logger.debug('reloaded config');
|
||||
|
||||
return validated.data;
|
||||
}
|
||||
|
||||
function handleError(error: ZodIssue) {
|
||||
|
|
|
@ -79,7 +79,7 @@ export function decryptToken(encryptedToken: string, secret: string): [number, s
|
|||
const encrypted = Buffer.from(encrypted64, 'base64').toString('ascii');
|
||||
|
||||
return [date, decrypt(encrypted, key)];
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,6 @@ function getDatasource(conf?: typeof config): void {
|
|||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
datasource = global.__datasource__;
|
||||
|
||||
if (!global.__datasource__ && !datasource) {
|
||||
|
|
|
@ -4,6 +4,7 @@ import useSWR from 'swr';
|
|||
import type { Response } from '../api/response';
|
||||
import { useUserStore } from '../store/user';
|
||||
import { isAdministrator } from '../role';
|
||||
import { useShallow } from 'zustand/shallow';
|
||||
|
||||
export default function useLogin(administratorOnly: boolean = false) {
|
||||
const router = useRouter();
|
||||
|
@ -11,7 +12,7 @@ export default function useLogin(administratorOnly: boolean = false) {
|
|||
fallbackData: { user: undefined },
|
||||
});
|
||||
|
||||
const [user, setUser] = useUserStore((state) => [state.user, state.setUser]);
|
||||
const [user, setUser] = useUserStore(useShallow((state) => [state.user, state.setUser]));
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.user) {
|
||||
|
|
|
@ -124,7 +124,7 @@ export type Zipline3Export = {
|
|||
|
||||
stats: {
|
||||
created_at: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
data: any;
|
||||
}[];
|
||||
};
|
||||
|
|
|
@ -125,7 +125,7 @@ function modifier(mod: string, value: unknown, tzlocale?: string): string {
|
|||
try {
|
||||
Intl.DateTimeFormat.supportedLocalesOf(locale);
|
||||
args[0] = locale;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
args[0] = undefined;
|
||||
logger.error(`invalid locale provided ${locale}`);
|
||||
}
|
||||
|
|
|
@ -103,7 +103,7 @@ export function humanTime(string: StringValue | string): Date | null {
|
|||
if (!mil) return null;
|
||||
|
||||
return new Date(Date.now() + mil);
|
||||
} catch (_) {
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { Title } from '@mantine/core';
|
||||
import { modals } from '@mantine/modals';
|
||||
|
||||
type WarningModalOptions = {
|
||||
|
@ -9,7 +8,7 @@ type WarningModalOptions = {
|
|||
|
||||
export function openWarningModal(options: WarningModalOptions) {
|
||||
modals.openConfirmModal({
|
||||
title: <Title order={3}>Are you sure?</Title>,
|
||||
title: 'Are you sure?',
|
||||
labels: {
|
||||
cancel: 'Cancel',
|
||||
confirm: options.confirmLabel,
|
||||
|
|
|
@ -10,8 +10,11 @@ import '@mantine/core/styles.css';
|
|||
import '@mantine/dates/styles.css';
|
||||
import '@mantine/dropzone/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import '@mantine/charts/styles.css';
|
||||
import 'mantine-datatable/styles.css';
|
||||
|
||||
import '@/styles/global.css';
|
||||
|
||||
import '@/components/render/code/HighlightCode.theme.css';
|
||||
|
||||
const fetcher = async (url: RequestInfo | URL) => {
|
||||
|
@ -35,6 +38,7 @@ export default function App({ Component, pageProps }: AppProps) {
|
|||
<title>Zipline</title>
|
||||
<meta name='viewport' content='minimum-scale=1, initial-scale=1, width=device-width' />
|
||||
<link rel='manifest' href='/manifest.json' />
|
||||
<link rel='icon' type='image/png' href='/favicon' />
|
||||
</Head>
|
||||
|
||||
<SWRConfig
|
||||
|
|
|
@ -21,7 +21,7 @@ import {
|
|||
Title,
|
||||
Image,
|
||||
} from '@mantine/core';
|
||||
import { hasLength, useForm } from '@mantine/form';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { notifications, showNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconBrandDiscordFilled,
|
||||
|
@ -74,8 +74,8 @@ export default function Login({ config }: InferGetServerSidePropsType<typeof get
|
|||
password: '',
|
||||
},
|
||||
validate: {
|
||||
username: hasLength({ min: 1 }, 'Username is required'),
|
||||
password: hasLength({ min: 1 }, 'Password is required'),
|
||||
username: (value) => (value.length > 1 ? null : 'Username is required'),
|
||||
password: (value) => (value.length > 1 ? null : 'Password is required'),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -134,7 +134,7 @@ export default function Login({ config }: InferGetServerSidePropsType<typeof get
|
|||
} else {
|
||||
mutate(data as Response['/api/user']);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch {
|
||||
setPasskeyErrored(true);
|
||||
setPasskeyLoading(false);
|
||||
}
|
||||
|
@ -171,12 +171,7 @@ export default function Login({ config }: InferGetServerSidePropsType<typeof get
|
|||
<>
|
||||
{willRedirect && !showLocalLogin && <LoadingOverlay visible />}
|
||||
|
||||
<Modal
|
||||
onClose={() => {}}
|
||||
title={<Title order={3}>Enter code</Title>}
|
||||
opened={totpOpen}
|
||||
withCloseButton={false}
|
||||
>
|
||||
<Modal onClose={() => {}} title='Enter code' opened={totpOpen} withCloseButton={false}>
|
||||
<Center>
|
||||
<PinInput
|
||||
data-autofocus
|
||||
|
|
|
@ -6,7 +6,7 @@ import { mutate } from 'swr';
|
|||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
const [setUser] = useUserStore((state) => [state.setUser]);
|
||||
const setUser = useUserStore((state) => state.setUser);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { hasLength, useForm } from '@mantine/form';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconPlus, IconX } from '@tabler/icons-react';
|
||||
import { InferGetServerSidePropsType } from 'next';
|
||||
|
@ -49,8 +49,8 @@ export default function Register({ config, invite }: InferGetServerSidePropsType
|
|||
tos: false,
|
||||
},
|
||||
validate: {
|
||||
username: hasLength({ min: 1 }, 'Username is required'),
|
||||
password: hasLength({ min: 1 }, 'Password is required'),
|
||||
username: (value) => (value.length < 1 ? 'Username is required' : null),
|
||||
password: (value) => (value.length < 1 ? 'Password is required' : null),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
TextInput,
|
||||
Title,
|
||||
} from '@mantine/core';
|
||||
import { hasLength, useForm } from '@mantine/form';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { notifications } from '@mantine/notifications';
|
||||
import { IconArrowBackUp, IconArrowForwardUp, IconCheck, IconX } from '@tabler/icons-react';
|
||||
import { GetServerSideProps } from 'next';
|
||||
|
@ -40,8 +40,8 @@ export default function Setup() {
|
|||
password: '',
|
||||
},
|
||||
validate: {
|
||||
username: hasLength({ min: 1 }, 'Username is required'),
|
||||
password: hasLength({ min: 1 }, 'Password is required'),
|
||||
username: (value) => (value.length < 1 ? 'Username is required' : null),
|
||||
password: (value) => (value.length < 1 ? 'Password is required' : null),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ import {
|
|||
Paper,
|
||||
PasswordInput,
|
||||
Text,
|
||||
Title,
|
||||
TypographyStylesProvider,
|
||||
} from '@mantine/core';
|
||||
import { IconDownload, IconInfoCircleFilled } from '@tabler/icons-react';
|
||||
|
@ -55,8 +54,6 @@ export default function ViewFileId({
|
|||
|
||||
const router = useRouter();
|
||||
|
||||
console.log(file);
|
||||
|
||||
const [passwordValue, setPassword] = useState<string>('');
|
||||
const [passwordError, setPasswordError] = useState<string>('');
|
||||
const [detailsOpen, setDetailsOpen] = useState<boolean>(false);
|
||||
|
@ -165,13 +162,7 @@ export default function ViewFileId({
|
|||
useEffect(() => setMounted(true), []);
|
||||
|
||||
return password && !pw ? (
|
||||
<Modal
|
||||
onClose={() => {}}
|
||||
opened={true}
|
||||
withCloseButton={false}
|
||||
centered
|
||||
title={<Title>Password required</Title>}
|
||||
>
|
||||
<Modal onClose={() => {}} opened={true} withCloseButton={false} centered title='Password required'>
|
||||
<PasswordInput
|
||||
description='This file is password protected, enter password to view it'
|
||||
required
|
||||
|
@ -372,7 +363,7 @@ export const getServerSideProps: GetServerSideProps<{
|
|||
)
|
||||
host = `https://${host}`;
|
||||
else host = `http://${host}`;
|
||||
} catch (e) {
|
||||
} catch {
|
||||
if (proto === 'https' || zConfig.core.returnHttpsUrls) host = `https://${host}`;
|
||||
else host = `http://${host}`;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { verifyPassword } from '@/lib/crypto';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { fetchApi } from '@/lib/fetchApi';
|
||||
import { Button, Modal, PasswordInput, Title } from '@mantine/core';
|
||||
import { Button, Modal, PasswordInput } from '@mantine/core';
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
|
@ -26,13 +26,7 @@ export default function ViewUrlId({ url, password }: InferGetServerSidePropsType
|
|||
};
|
||||
|
||||
return password ? (
|
||||
<Modal
|
||||
onClose={() => {}}
|
||||
opened={true}
|
||||
withCloseButton={false}
|
||||
centered
|
||||
title={<Title>Password required</Title>}
|
||||
>
|
||||
<Modal onClose={() => {}} opened={true} withCloseButton={false} centered title='Password required'>
|
||||
<PasswordInput
|
||||
description='This link is password protected, enter password to view it'
|
||||
required
|
||||
|
|
|
@ -85,7 +85,7 @@ async function main() {
|
|||
|
||||
await server.register(fastifyStatic, {
|
||||
serve: false,
|
||||
root: '/',
|
||||
root: config.core.tempDirectory,
|
||||
});
|
||||
|
||||
if (config.ratelimit.enabled) {
|
||||
|
@ -225,3 +225,10 @@ declare module 'fastify' {
|
|||
tasks: Tasks;
|
||||
}
|
||||
}
|
||||
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.log('AAA', reason, promise);
|
||||
});
|
||||
process.on('uncaughtException', (reason) => {
|
||||
console.log('aaa', reason);
|
||||
});
|
||||
|
|
|
@ -36,8 +36,7 @@ export default fastifyPlugin(
|
|||
|
||||
if (!file.completed) return res.badRequest('Export is not completed');
|
||||
|
||||
const path = join(config.core.tempDirectory, file.path);
|
||||
return res.sendFile(path);
|
||||
return res.sendFile(file.path);
|
||||
}
|
||||
|
||||
return res.send(exports);
|
||||
|
|
14
src/server/routes/favicon.ts
Normal file
14
src/server/routes/favicon.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import fastifyPlugin from 'fastify-plugin';
|
||||
import { join } from 'path';
|
||||
|
||||
export const PATH = '/favicon';
|
||||
export default fastifyPlugin(
|
||||
(server, _, done) => {
|
||||
server.get(PATH, (_, res) => {
|
||||
return res.sendFile('favicon-32x32.png', join(process.cwd(), 'public'));
|
||||
});
|
||||
|
||||
done();
|
||||
},
|
||||
{ name: PATH },
|
||||
);
|
7
src/styles/global.css
Normal file
7
src/styles/global.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
.mantine-Modal-title {
|
||||
line-height: 1;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
font-size: var(--mantine-font-size-xl);
|
||||
}
|
|
@ -6,14 +6,12 @@ export default defineConfig(async (_) => {
|
|||
{
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
treeshake: true,
|
||||
clean: false,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
entryPoints: await glob('./src/**/*.ts', {
|
||||
entry: await glob('./src/**/*.ts', {
|
||||
ignore: ['./src/components/**/*.ts', './src/pages/**/*.ts'],
|
||||
}),
|
||||
outDir: 'build',
|
||||
external: ['argon2'],
|
||||
},
|
||||
];
|
||||
});
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig([
|
||||
{
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
treeshake: true,
|
||||
clean: false,
|
||||
sourcemap: true,
|
||||
entryPoints: {
|
||||
ctl: 'src/ctl/index.ts',
|
||||
},
|
||||
outDir: 'build',
|
||||
bundle: true,
|
||||
minify: true,
|
||||
},
|
||||
]);
|
Loading…
Add table
Add a link
Reference in a new issue