fix: a bunch of random stuff

This commit is contained in:
diced 2024-12-20 00:07:33 -08:00
parent dcb4a4e9e7
commit 12fcff1a14
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
82 changed files with 9015 additions and 5875 deletions

View file

@ -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',
},
};

View file

@ -36,4 +36,4 @@ services:
- './themes:/zipline/themes'
volumes:
pgdata:
pgdata:

86
eslint.config.mjs Normal file
View 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',
},
},
];

View file

@ -1381,4 +1381,4 @@
["zir", ["application/vnd.zul"]],
["zirz", ["application/vnd.zul"]],
["zmm", ["application/vnd.handheld-entertainment+xml"]]
]
]

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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

View file

@ -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.',

View file

@ -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');

View file

@ -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 &quot;{file.name}&quot;</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'

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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',

View file

@ -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')} />

View file

@ -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')} />

View file

@ -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>
);
}

View file

@ -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',

View file

@ -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'

View file

@ -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}
>

View file

@ -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',

View file

@ -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')} />

View file

@ -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

View file

@ -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);

View file

@ -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'

View file

@ -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>
);

View file

@ -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>

View file

@ -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>
);

View file

@ -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;
}

View file

@ -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>
);

View file

@ -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,
});

View file

@ -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');

View file

@ -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]);

View file

@ -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,

View file

@ -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');
}
}

View file

@ -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) => {

View file

@ -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 () => {

View file

@ -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: {

View file

@ -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}

View file

@ -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) => (

View file

@ -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'>

View file

@ -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,

View file

@ -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' },

View file

@ -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' },

View file

@ -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

View file

@ -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={() => {

View file

@ -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

View file

@ -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 () => {

View file

@ -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),
},
});

View file

@ -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 }>({

View file

@ -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('');

View file

@ -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>

View file

@ -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'}>

View file

@ -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'}>

View file

@ -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 &quot;{url.vanity ?? url.code}&quot;</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'

View file

@ -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')} />

View file

@ -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[]>>(
{

View file

@ -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>

View file

@ -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}&apos;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',

View file

@ -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

View file

@ -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: ' ' }) ?? '';
}

View file

@ -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) {

View file

@ -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;
}
}

View file

@ -34,7 +34,6 @@ function getDatasource(conf?: typeof config): void {
}
}
// eslint-disable-next-line prefer-const
datasource = global.__datasource__;
if (!global.__datasource__ && !datasource) {

View file

@ -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) {

View file

@ -124,7 +124,7 @@ export type Zipline3Export = {
stats: {
created_at: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data: any;
}[];
};

View file

@ -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}`);
}

View file

@ -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;
}
}

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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 () => {

View file

@ -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),
},
});

View file

@ -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),
},
});

View file

@ -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}`;
}

View file

@ -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

View file

@ -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);
});

View file

@ -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);

View 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
View 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);
}

View file

@ -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'],
},
];
});

View file

@ -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,
},
]);