1
Fork 0
mirror of https://github.com/diced/zipline.git synced 2025-04-11 23:31:17 -05:00

feat: overhaul qs system

This commit is contained in:
diced 2025-04-03 12:32:27 -07:00
parent e8207addba
commit 9611e6d5a5
No known key found for this signature in database
GPG key ID: 436B2B0FA0DCA354
19 changed files with 117 additions and 133 deletions

View file

@ -65,6 +65,7 @@
"ms": "^2.1.3",
"multer": "1.4.5-lts.1",
"next": "^15.2.4",
"nuqs": "^2.4.1",
"otplib": "^12.0.1",
"prisma": "^6.4.1",
"qrcode": "^1.5.4",

33
pnpm-lock.yaml generated
View file

@ -143,6 +143,9 @@ importers:
next:
specifier: ^15.2.4
version: 15.2.4(@babel/core@7.26.9)(@opentelemetry/api@1.7.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.86.1)
nuqs:
specifier: ^2.4.1
version: 2.4.1(next@15.2.4(@babel/core@7.26.9)(@opentelemetry/api@1.7.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.86.1))(react@19.0.0)
otplib:
specifier: ^12.0.1
version: 12.0.1
@ -3598,6 +3601,9 @@ packages:
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
engines: {node: '>=16 || 14 >=14.17'}
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
mkdirp@0.5.6:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
@ -3673,6 +3679,24 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
nuqs@2.4.1:
resolution: {integrity: sha512-u6sngTspqDe3jWHtcmqHQg3dl35niizCZAsm5gy7PBlgG2rwl71Dp2QUv5hwBaWKI9qz0wqILZY86TsRxq66SQ==}
peerDependencies:
'@remix-run/react': '>=2'
next: '>=14.2.0'
react: '>=18.2.0 || ^19.0.0-0'
react-router: ^6 || ^7
react-router-dom: ^6 || ^7
peerDependenciesMeta:
'@remix-run/react':
optional: true
next:
optional: true
react-router:
optional: true
react-router-dom:
optional: true
nwsapi@2.2.16:
resolution: {integrity: sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==}
@ -8955,6 +8979,8 @@ snapshots:
minipass@7.1.2: {}
mitt@3.0.1: {}
mkdirp@0.5.6:
dependencies:
minimist: 1.2.8
@ -9029,6 +9055,13 @@ snapshots:
normalize-path@3.0.0: {}
nuqs@2.4.1(next@15.2.4(@babel/core@7.26.9)(@opentelemetry/api@1.7.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.86.1))(react@19.0.0):
dependencies:
mitt: 3.0.1
react: 19.0.0
optionalDependencies:
next: 15.2.4(@babel/core@7.26.9)(@opentelemetry/api@1.7.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(sass@1.86.1)
nwsapi@2.2.16: {}
object-assign@4.1.1: {}

View file

@ -15,6 +15,7 @@ import { useEffect, useState } from 'react';
import { renderMode } from '../pages/upload/renderMode';
import Render from '../render/Render';
import fileIcon from './fileIcon';
import { parseAsStringLiteral, useQueryState } from 'nuqs';
function PlaceholderContent({ text, Icon }: { text: string; Icon: Icon }) {
return (
@ -77,6 +78,8 @@ export default function DashboardFileType({
code?: boolean;
allowZoom?: boolean;
}) {
const [overrideType] = useQueryState('otype', parseAsStringLiteral(['video', 'audio', 'image', 'text']));
const disableMediaPreview = useSettingsStore((state) => state.settings.disableMediaPreview);
const dbFile = 'id' in file;
@ -129,7 +132,7 @@ export default function DashboardFileType({
if (code) {
setType('text');
gettext();
} else if (type === 'text') {
} else if (overrideType === 'text' || type === 'text') {
gettext();
} else {
return;
@ -153,7 +156,7 @@ export default function DashboardFileType({
</Paper>
);
switch (type) {
switch (overrideType || type) {
case 'video':
return show ? (
<video

View file

@ -5,8 +5,8 @@ import { ActionIcon, Badge, Button, Card, Group, Modal, Paper, Stack, Text, Tool
import { showNotification } from '@mantine/notifications';
import { IncompleteFileStatus } from '@prisma/client';
import { IconFileDots, IconTrashFilled } from '@tabler/icons-react';
import { useRouter } from 'next/router';
import { ReactNode, useEffect, useState } from 'react';
import { parseAsBoolean, useQueryState } from 'nuqs';
import { ReactNode } from 'react';
import useSWR from 'swr';
const badgeMap: Record<IncompleteFileStatus, ReactNode> = {
@ -33,9 +33,7 @@ const badgeMap: Record<IncompleteFileStatus, ReactNode> = {
};
export default function PendingFilesButton() {
const router = useRouter();
const [open, setOpen] = useState(router.query.pending !== undefined);
const [open, setOpen] = useQueryState('popen', parseAsBoolean.withDefault(false));
const { data: incompleteFiles, mutate } = useSWR<
Extract<IncompleteFile[], Response['/api/user/files/incomplete']>
@ -68,15 +66,6 @@ export default function PendingFilesButton() {
mutate();
};
useEffect(() => {
if (open) {
router.push({ query: { ...router.query, pending: 'true' } }, undefined, { shallow: true });
} else {
delete router.query.pending;
router.push({ query: router.query }, undefined, { shallow: true });
}
}, [open]);
return (
<>
<Modal opened={open} onClose={() => setOpen(false)} title='Pending Files'>

View file

@ -26,7 +26,7 @@ export default function CreateTagModal({ open, onClose }: { open: boolean; onClo
const color = values.color.trim() === '' ? colorHash(values.name) : values.color.trim();
if (!color.startsWith('#')) {
form.setFieldError('color', 'Color must start with #');
return form.setFieldError('color', 'Color must start with #');
}
const { data, error } = await fetchApi<Extract<Response['/api/user/tags'], Tag>>(

View file

@ -1,22 +1,20 @@
import { mutateFiles } from '@/components/file/actions';
import { Response } from '@/lib/api/response';
import { Tag } from '@/lib/db/models/tag';
import { ActionIcon, Group, Modal, Paper, Stack, Text, Title, Tooltip } from '@mantine/core';
import { IconPencil, IconPlus, IconTagOff, IconTags, IconTrashFilled } from '@tabler/icons-react';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import useSWR from 'swr';
import TagPill from './TagPill';
import { fetchApi } from '@/lib/fetchApi';
import { ActionIcon, Group, Modal, Paper, Stack, Text, Title, Tooltip } from '@mantine/core';
import { showNotification } from '@mantine/notifications';
import { IconPencil, IconPlus, IconTagOff, IconTags, IconTrashFilled } from '@tabler/icons-react';
import { parseAsBoolean, useQueryState } from 'nuqs';
import { useState } from 'react';
import useSWR from 'swr';
import CreateTagModal from './CreateTagModal';
import EditTagModal from './EditTagModal';
import { mutateFiles } from '@/components/file/actions';
import TagPill from './TagPill';
export default function TagsButton() {
const router = useRouter();
const [open, setOpen] = useState(router.query.tags !== undefined);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [open, setOpen] = useQueryState('topen', parseAsBoolean.withDefault(false));
const [createModalOpen, setCreateModalOpen] = useQueryState('ctopen', parseAsBoolean.withDefault(false));
const [selectedTag, setSelectedTag] = useState<Tag | null>(null);
const { data: tags, mutate } = useSWR<Extract<Tag[], Response['/api/user/tags']>>('/api/user/tags');
@ -44,15 +42,6 @@ export default function TagsButton() {
mutateFiles();
};
useEffect(() => {
if (open) {
router.push({ query: { ...router.query, tags: 'true' } }, undefined, { shallow: true });
} else {
delete router.query.tags;
router.push({ query: router.query }, undefined, { shallow: true });
}
}, [open]);
return (
<>
<CreateTagModal open={createModalOpen} onClose={() => setCreateModalOpen(false)} />

View file

@ -7,7 +7,17 @@ type ApiPaginationOptions = {
filter?: string;
perpage?: number;
favorite?: boolean;
sort?: keyof Prisma.FileOrderByWithAggregationInput;
sort?:
| 'name'
| 'id'
| 'createdAt'
| 'updatedAt'
| 'deletesAt'
| 'originalName'
| 'size'
| 'type'
| 'views'
| 'favorite';
order?: 'asc' | 'desc';
id?: string;
search?: {

View file

@ -12,47 +12,24 @@ import {
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';
import Link from 'next/link';
import { parseAsInteger, useQueryState } from 'nuqs';
import { useApiPagination } from '../useApiPagination';
const DashboardFile = dynamic(() => import('@/components/file/DashboardFile'), {
loading: () => <Skeleton height={350} animate />,
});
export default function FavoriteFiles() {
const router = useRouter();
const [page, setPage] = useQueryState('fpage', parseAsInteger.withDefault(1));
const [page, setPage] = useState<number>(
router.query.favoritePage ? parseInt(router.query.favoritePage as string) : 1,
);
const { data, isLoading } = useApiPagination({
page,
favorite: true,
filter: 'dashboard',
});
useEffect(() => {
router.replace(
{
query: {
...router.query,
favoritePage: page,
},
},
undefined,
{ shallow: true },
);
}, [page]);
if (!isLoading && !data?.page.length && router.query.favoritePage) {
delete router.query.favoritePage;
router.replace({ query: router.query }, undefined, { shallow: true });
setPage(1);
}
if (!isLoading && !data?.page.length) {
return null;
}

View file

@ -28,7 +28,6 @@ import {
useCombobox,
} from '@mantine/core';
import { useClipboard } from '@mantine/hooks';
import type { Prisma } from '@prisma/client';
import {
IconCopy,
IconExternalLink,
@ -39,7 +38,7 @@ import {
} from '@tabler/icons-react';
import { DataTable } from 'mantine-datatable';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { parseAsBoolean, parseAsInteger, parseAsStringLiteral, useQueryState } from 'nuqs';
import { useEffect, useReducer, useState } from 'react';
import useSWR from 'swr';
import { bulkDelete, bulkFavorite } from '../bulk';
@ -179,7 +178,6 @@ function TagsFilter({
}
export default function FileTable({ id }: { id?: string }) {
const router = useRouter();
const clipboard = useClipboard();
const warnDeletion = useSettingsStore((state) => state.settings.warnDeletion);
@ -187,13 +185,30 @@ export default function FileTable({ id }: { id?: string }) {
'/api/user/folders?noincl=true',
);
const [page, setPage] = useState<number>(router.query.page ? parseInt(router.query.page as string) : 1);
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
const [perpage, setPerpage] = useState<number>(20);
const [sort, setSort] = useState<keyof Prisma.FileOrderByWithAggregationInput>('createdAt');
const [order, setOrder] = useState<'asc' | 'desc'>('desc');
const [sort, setSort] = useQueryState(
'sort',
parseAsStringLiteral([
'id',
'createdAt',
'updatedAt',
'deletesAt',
'name',
'originalName',
'size',
'type',
'views',
'favorite',
]).withDefault('createdAt'),
);
const [order, setOrder] = useQueryState<'asc' | 'desc'>(
'order',
parseAsStringLiteral(['asc', 'desc']).withDefault('desc'),
);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [idSearchOpen, setIdSearchOpen] = useState(false);
const [idSearchOpen, setIdSearchOpen] = useQueryState('idsearch', parseAsBoolean.withDefault(false));
const [searchField, setSearchField] = useState<'name' | 'originalName' | 'type' | 'tags' | 'id'>('name');
const [searchQuery, setSearchQuery] = useReducer(
(state: ReducerQuery['state'], action: ReducerQuery['action']) => {
@ -253,19 +268,6 @@ export default function FileTable({ id }: { id?: string }) {
}),
});
useEffect(() => {
router.replace(
{
query: {
...router.query,
page: page,
},
},
undefined,
{ shallow: true },
);
}, [page]);
useEffect(() => {
if (data && selectedFile) {
const file = data.page.find((x) => x.id === selectedFile.id);
@ -546,7 +548,7 @@ export default function FileTable({ id }: { id?: string }) {
direction: order,
}}
onSortStatusChange={(data) => {
setSort(data.columnAccessor as keyof Prisma.FileOrderByWithAggregationInput);
setSort(data.columnAccessor as any);
setOrder(data.direction);
}}
onCellClick={({ record }) => setSelectedFile(record)}

View file

@ -17,6 +17,7 @@ import Link from 'next/link';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import { useApiPagination } from '../useApiPagination';
import { parseAsInteger, useQueryState } from 'nuqs';
const DashboardFile = dynamic(() => import('@/components/file/DashboardFile'), {
loading: () => <Skeleton height={350} animate />,
@ -27,7 +28,7 @@ const PER_PAGE_OPTIONS = [9, 12, 15, 30, 45];
export default function Files({ id }: { id?: string }) {
const router = useRouter();
const [page, setPage] = useState<number>(router.query.page ? parseInt(router.query.page as string) : 1);
const [page, setPage] = useQueryState('page', parseAsInteger.withDefault(1));
const [perpage, setPerpage] = useState<number>(15);
const [cachedPages, setCachedPages] = useState<number>(1);
@ -43,19 +44,6 @@ export default function Files({ id }: { id?: string }) {
}
}, [data?.pages]);
useEffect(() => {
router.replace(
{
query: {
...router.query,
page: page,
},
},
undefined,
{ shallow: true },
);
}, [page]);
const from = (page - 1) * perpage + 1;
const to = Math.min(page * perpage, data?.total ?? 0);
const totalRecords = data?.total ?? 0;

View file

@ -13,35 +13,17 @@ import {
} 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 { parseAsInteger, useQueryState } from 'nuqs';
import { useApiPagination } from '../files/useApiPagination';
export default function FavoriteFiles() {
const router = useRouter();
const [page, setPage] = useState<number>(
router.query.favoritePage ? parseInt(router.query.favoritePage as string) : 1,
);
const [page, setPage] = useQueryState('fpage', parseAsInteger.withDefault(1));
const { data, isLoading } = useApiPagination({
page,
favorite: true,
filter: 'dashboard',
});
useEffect(() => {
router.replace(
{
query: {
...router.query,
favoritePage: page,
},
},
undefined,
{ shallow: true },
);
}, [page]);
if (!isLoading && data?.page.length === 0) return null;
return (

View file

@ -11,11 +11,12 @@ import { useState } from 'react';
import { mutate } from 'swr';
import FolderGridView from './views/FolderGridView';
import FolderTableView from './views/FolderTableView';
import { parseAsBoolean, useQueryState } from 'nuqs';
export default function DashboardFolders() {
const view = useViewStore((state) => state.folders);
const [open, setOpen] = useState(false);
const [open, setOpen] = useQueryState('cfopen', parseAsBoolean.withDefault(false));
const form = useForm({
initialValues: {

View file

@ -11,10 +11,11 @@ import { Response } from '@/lib/api/response';
import { notifications } from '@mantine/notifications';
import { Invite } from '@/lib/db/models/invite';
import { mutate } from 'swr';
import { parseAsBoolean, useQueryState } from 'nuqs';
export default function DashboardInvites() {
const view = useViewStore((state) => state.invites);
const [open, setOpen] = useState(false);
const [open, setOpen] = useQueryState('ciopen', parseAsBoolean.withDefault(false));
const form = useForm<{
maxUses: number | '';

View file

@ -32,6 +32,7 @@ import {
} from '@tabler/icons-react';
import ms, { StringValue } from 'ms';
import Link from 'next/link';
import { parseAsBoolean, useQueryState } from 'nuqs';
import { useEffect, useState } from 'react';
import useSWR from 'swr';
import { useShallow } from 'zustand/shallow';
@ -39,7 +40,7 @@ import { useShallow } from 'zustand/shallow';
export default function UploadOptionsButton({ folder, numFiles }: { folder?: string; numFiles: number }) {
const config = useConfig();
const [opened, setOpen] = useState(false);
const [opened, setOpen] = useQueryState('upopen', parseAsBoolean.withDefault(false));
const [options, ephemeral, setOption, setEphemeral, changes, clearEphemeral, clearOptions] =
useUploadOptionsStore(
useShallow((state) => [

View file

@ -1,12 +1,13 @@
export enum RenderMode {
Katex,
Markdown,
Highlight,
Katex = 'katex',
Markdown = 'md',
Highlight = 'hl',
}
export function renderMode(extension: string) {
switch (extension) {
case 'tex':
case 'katex':
return RenderMode.Katex;
case 'md':
return RenderMode.Markdown;

View file

@ -1,5 +1,6 @@
import GridTableSwitcher from '@/components/GridTableSwitcher';
import { Response } from '@/lib/api/response';
import { Url } from '@/lib/db/models/url';
import { fetchApi } from '@/lib/fetchApi';
import { useViewStore } from '@/lib/store/view';
import {
@ -23,17 +24,16 @@ import { modals } from '@mantine/modals';
import { notifications } from '@mantine/notifications';
import { IconClipboardCopy, IconExternalLink, IconLink, IconLinkOff } from '@tabler/icons-react';
import Link from 'next/link';
import { useState } from 'react';
import { parseAsBoolean, useQueryState } from 'nuqs';
import { mutate } from 'swr';
import UrlGridView from './views/UrlGridView';
import UrlTableView from './views/UrlTableView';
import { Url } from '@/lib/db/models/url';
export default function DashboardURLs() {
const clipboard = useClipboard();
const view = useViewStore((state) => state.urls);
const [open, setOpen] = useState(false);
const [open, setOpen] = useQueryState('cuopen', parseAsBoolean.withDefault(false));
const form = useForm<{
url: string;

View file

@ -22,7 +22,7 @@ import {
import { useForm } from '@mantine/form';
import { notifications } from '@mantine/notifications';
import { IconPhotoMinus, IconUserCancel, IconUserPlus } from '@tabler/icons-react';
import { useState } from 'react';
import { parseAsBoolean, useQueryState } from 'nuqs';
import { mutate } from 'swr';
import UserGridView from './views/UserGridView';
import UserTableView from './views/UserTableView';
@ -30,7 +30,7 @@ import UserTableView from './views/UserTableView';
export default function DashboardUsers() {
const currentUser = useUserStore((state) => state.user);
const view = useViewStore((state) => state.users);
const [open, setOpen] = useState(false);
const [open, setOpen] = useQueryState('cuseropen', parseAsBoolean.withDefault(false));
const form = useForm<{
username: string;

View file

@ -5,6 +5,7 @@ import { useState } from 'react';
import KaTeX from './KaTeX';
import Markdown from './Markdown';
import HighlightCode from './code/HighlightCode';
import { parseAsStringEnum, parseAsStringLiteral, useQueryState } from 'nuqs';
export function RenderAlert({
renderer,
@ -46,9 +47,11 @@ export default function Render({
language: string;
code: string;
}) {
const [overrideRender] = useQueryState('orender', parseAsStringEnum<RenderMode>(Object.values(RenderMode)));
const [highlight, setHighlight] = useState(false);
switch (mode) {
switch (overrideRender || mode) {
case RenderMode.Katex:
return (
<>

View file

@ -1,4 +1,5 @@
import { AppProps } from 'next/app';
import { NuqsAdapter } from 'nuqs/adapters/next/pages';
import Head from 'next/head';
import { SWRConfig } from 'swr';
import { ModalsProvider } from '@mantine/modals';
@ -60,7 +61,9 @@ export default function App({
}}
>
<Notifications zIndex={100000000} />
<Component {...pageProps} />
<NuqsAdapter>
<Component {...pageProps} />
</NuqsAdapter>
</ModalsProvider>
</Theming>
</SWRConfig>