This commit is contained in:
diced
2026-04-01 17:53:22 -07:00
parent 67a9fe34b4
commit 8128e3deb0
3 changed files with 76 additions and 26 deletions

View File

@@ -1,7 +1,8 @@
import { type Response } from '@/lib/api/response';
import { useQueryState } from '@/lib/client/hooks/useQueryState';
import { useTitle } from '@/lib/client/hooks/useTitle';
import { Folder } from '@/lib/db/models/folder';
import { FolderBreadcrumb } from '@/lib/folderHierarchy';
import { useTitle } from '@/lib/client/hooks/useTitle';
import {
ActionIcon,
Anchor,
@@ -9,6 +10,8 @@ import {
Card,
Container,
Group,
Pagination,
Select,
SimpleGrid,
Skeleton,
Stack,
@@ -16,7 +19,7 @@ import {
Title,
} from '@mantine/core';
import { IconFolder, IconUpload } from '@tabler/icons-react';
import { lazy, Suspense } from 'react';
import { lazy, Suspense, useMemo, useState } from 'react';
import { Link, Params, useLoaderData, useNavigate } from 'react-router-dom';
const DashboardFile = lazy(() => import('@/components/file/DashboardFile'));
@@ -58,6 +61,8 @@ function PublicFolderCard({ folder }: { folder: Partial<Folder> }) {
);
}
const PER_PAGE_OPTIONS = [9, 12, 15, 30, 45];
export function Component() {
const { folder } = useLoaderData<typeof loader>();
const navigate = useNavigate();
@@ -81,6 +86,21 @@ export function Component() {
const breadcrumbs = buildBreadcrumbs();
const children = (folder.children ?? []) as Partial<Folder>[];
const [perpage, setPerpage] = useState(15);
const [page, setPage] = useQueryState('page', 1);
const from = (page - 1) * perpage + 1;
const to = Math.min(page * perpage, folder.files?.length ?? 0);
const totalRecords = folder.files?.length ?? 0;
const cachedPages = Math.ceil(totalRecords / perpage);
const visible = useMemo(() => {
if (!folder.files) return [];
const start = (page - 1) * perpage;
return folder.files.slice(start, start + perpage);
}, [folder.files, page, perpage]);
return (
<>
<Container my='lg'>
@@ -132,7 +152,7 @@ export function Component() {
</>
)}
{(folder.files?.length ?? 0) > 0 && (
{(visible.length ?? 0) > 0 && (
<>
<Title order={3} mt='md' mb='sm'>
Files
@@ -145,7 +165,7 @@ export function Component() {
}}
spacing='md'
>
{folder.files?.map((file: any) => (
{visible.map((file: any) => (
<Suspense fallback={<Skeleton height={350} animate />} key={file.id}>
<DashboardFile file={file} reduce />
</Suspense>
@@ -159,6 +179,33 @@ export function Component() {
This folder is empty.
</Text>
)}
<Group justify='space-between' align='center' mt='md'>
<Text size='sm'>{`${from} - ${to} / ${totalRecords} files`}</Text>
<Group gap='sm'>
<Select
value={perpage.toString()}
data={PER_PAGE_OPTIONS.map((val) => ({ value: val.toString(), label: `${val}` }))}
onChange={(value) => {
setPerpage(Number(value));
setPage(1);
}}
w={80}
size='xs'
variant='filled'
/>
<Pagination
value={page}
onChange={setPage}
total={cachedPages}
size='sm'
withControls
withEdges
/>
</Group>
</Group>
</Container>
</>
);

View File

@@ -21,3 +21,26 @@ export function zValidatePath(val: string | undefined, ctx: z.RefinementCtx) {
export const zStringTrimmed = z.string().trim().min(1);
export const zQsBoolean = z.enum(['true', 'false']).transform((val) => val === 'true');
export const paginationQs = z.object({
page: z.coerce.number(),
perpage: z.coerce.number().default(15),
filter: z.enum(['dashboard', 'none', 'all']).optional().default('none'),
favorite: zQsBoolean.default(false).optional(),
sortBy: z
.enum([
'id',
'createdAt',
'updatedAt',
'deletesAt',
'name',
'originalName',
'size',
'type',
'views',
'favorite',
])
.optional()
.default('createdAt'),
order: z.enum(['asc', 'desc']).optional().default('desc'),
});

View File

@@ -2,7 +2,7 @@ import { ApiError } from '@/lib/api/errors';
import { prisma } from '@/lib/db';
import { File, cleanFiles, fileSchema, fileSelect } from '@/lib/db/models/file';
import { canInteract } from '@/lib/role';
import { zQsBoolean } from '@/lib/validation';
import { paginationQs, zQsBoolean } from '@/lib/validation';
import { userMiddleware } from '@/server/middleware/user';
import typedPlugin from '@/server/typedPlugin';
import z from 'zod';
@@ -29,27 +29,7 @@ export default typedPlugin(
schema: {
description:
'List, filter, and search files for the authenticated user (or another user if permitted).',
querystring: z.object({
page: z.coerce.number(),
perpage: z.coerce.number().default(15),
filter: z.enum(['dashboard', 'none', 'all']).optional().default('none'),
favorite: zQsBoolean.default(false).optional(),
sortBy: z
.enum([
'id',
'createdAt',
'updatedAt',
'deletesAt',
'name',
'originalName',
'size',
'type',
'views',
'favorite',
])
.optional()
.default('createdAt'),
order: z.enum(['asc', 'desc']).optional().default('desc'),
querystring: paginationQs.extend({
searchField: z.enum(['name', 'originalName', 'type', 'tags', 'id']).optional().default('name'),
searchQuery: z.string().optional(),
id: z.string().optional(),