Skip to content

Commit

Permalink
feat(web): add pagination to table
Browse files Browse the repository at this point in the history
  • Loading branch information
yjl9903 committed Oct 1, 2024
1 parent b8cd557 commit 19d2ef3
Show file tree
Hide file tree
Showing 8 changed files with 227 additions and 49 deletions.
102 changes: 102 additions & 0 deletions apps/frontend/web/app/components/Resources/pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import clsx from 'clsx';

import { formatChinaTime } from './utils';
import { Link } from '@remix-run/react';

export interface PaginationProps {
page: number;

complete: boolean;

timestamp: Date;

link?: (page: number) => string;

navigate?: (page: number) => void | Promise<void>;
}

export const Pagination = (props: PaginationProps) => {
const { page, complete, timestamp } = props;
const isPrev = page > 1;
const isNext = !complete;
const pages = isPrev ? [page - 1, page, page + 1, page + 2, page + 3] : [page, page + 1, page + 2, page + 3, page + 4];

if (page === 1 && complete) {
// Only one page data, no pagination
return;
} else {
return (
<div className="mt-4 flex lt-md:flex-col font-sm">
<div className="text-base-400 py-1 pl3 lt-sm:pl1">
<span className="mr1 text-sm i-carbon-update-now op-80"></span>
<span className="select-none">数据更新于 </span>
<span>{formatChinaTime(timestamp)}</span>
</div>
<div className="flex-auto"></div>
<div className="flex lt-md:(mt-4 justify-center) items-center gap-2 text-base-500">
<PageItem
page={page - 1}
link={props.link}
navigate={props.navigate}
className={clsx(
isPrev || 'hidden',
'block text-link-active underline decoration-dotted underline-offset-4'
)}
>
<span>上一页</span>
</PageItem>
{page > 2 && <span className="select-none i-ant-design:ellipsis-outlined"></span>}
{pages.map((p) => (
<PageItem
key={p}
page={p}
link={props.link}
navigate={props.navigate}
className={clsx(
'block',
p === page && 'text-pink-600'
)}
>
<span>{p}</span>
</PageItem>
))}
{isNext && <span className="select-none i-ant-design:ellipsis-outlined"></span>}
<PageItem
page={page + 1}
link={props.link}
navigate={props.navigate}
className={clsx(
isNext || 'hidden',
'block text-link-active underline decoration-dotted underline-offset-4'
)}
>
<span>下一页</span>
</PageItem>
</div>
</div>
);
}
};

const PageItem = (
props: Pick<PaginationProps, 'page' | 'link' | 'navigate'> & React.HTMLAttributes<any>
) => {
const { page, navigate, link } = props;
const className = 'px-2 py-1 rounded-md hover:bg-gray-100 select-none cursor-pointer';

if (link) {
return (
<Link to={link(page)} className={clsx(className, props.className)}>
{props.children}
</Link>
);
} else if (navigate) {
return (
<div className={clsx(className, props.className)} onClick={() => navigate(page)}>
{props.children}
</div>
);
} else {
return <div className={clsx(className, props.className)}>{props.children}</div>;
}
};
78 changes: 44 additions & 34 deletions apps/frontend/web/app/components/Resources/table.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import type { Resource } from 'animegarden';
import { NavLink, useLocation, type Location } from '@remix-run/react';

import { memo } from 'react';
import { formatInTimeZone } from 'date-fns-tz';

import type { Resource } from 'animegarden';

import { getPikPakUrlChecker } from '@/utils';
import { DisplayType, DisplayTypeColor, DisplayTypeIcon } from '@/constant';

import { Tag } from './tag';
import { NavLink, useLocation, type Location } from '@remix-run/react';
import { formatChinaTime } from './utils';
import { Pagination, PaginationProps } from './pagination';

export interface ResourcesTableProps {
export interface ResourcesTableProps extends PaginationProps {
className?: string;

resources: Resource<{ tracker: true }>[];
Expand All @@ -31,37 +33,46 @@ export default function ResourcesTable(props: ResourcesTableProps) {
const { resources, className } = props;

return (
<div className={'overflow-y-auto w-full' + (className ? ' ' + className : '')}>
<table className="resources-table border-collapse min-y-[1080px] w-full">
<colgroup>
{/* <col className="py3 w-[160px] min-w-[100px] lt-lg:w-[100px] lt-sm:w-[100px]" /> */}
<col className="text-left xl:min-w-[600px] lg:min-w-[480px]" />
<col className="w-max whitespace-nowrap" />
<col className="w-max whitespace-nowrap" />
</colgroup>
<thead className="resources-table-head border-b border-b-2 text-lg lt-lg:text-base">
<tr className="">
{/* <th className="py3 w-[160px] min-w-[100px] lt-lg:w-[100px] lt-sm:w-[100px]">
<div>
<div className={'overflow-y-auto w-full' + (className ? ' ' + className : '')}>
<table className="resources-table border-collapse min-y-[1080px] w-full">
<colgroup>
{/* <col className="py3 w-[160px] min-w-[100px] lt-lg:w-[100px] lt-sm:w-[100px]" /> */}
<col className="text-left xl:min-w-[600px] lg:min-w-[480px]" />
<col className="w-max whitespace-nowrap" />
<col className="w-max whitespace-nowrap" />
</colgroup>
<thead className="resources-table-head border-b border-b-2 text-lg lt-lg:text-base">
<tr className="">
{/* <th className="py3 w-[160px] min-w-[100px] lt-lg:w-[100px] lt-sm:w-[100px]">
发布时间
</th> */}
<th className="py3 pl3 lt-sm:pl1 text-left xl:min-w-[600px] lg:min-w-[480px]">
<div className="flex">
<div className="flex-shrink-0 mr3 flex justify-center items-center w-[32px]">
<span className="text-2xl i-carbon-types"></span>
<th className="py3 pl3 lt-sm:pl1 text-left xl:min-w-[600px] lg:min-w-[480px]">
<div className="flex">
<div className="flex-shrink-0 mr3 flex justify-center items-center w-[32px]">
<span className="text-2xl i-carbon-types"></span>
</div>
<div>资源</div>
</div>
<div>资源</div>
</div>
</th>
<th className="py3">发布者</th>
<th className="py3 px2 text-center w-max">播放</th>
</tr>
</thead>
<tbody className="resources-table-body divide-y border-b text-base lt-lg:text-sm">
{resources.map((r) => (
<ResourceItem key={`${r.provider}/${r.providerId}`} resource={r}></ResourceItem>
))}
</tbody>
</table>
</th>
<th className="py3">发布者</th>
<th className="py3 px2 text-center w-max">播放</th>
</tr>
</thead>
<tbody className="resources-table-body divide-y border-b text-base lt-lg:text-sm">
{resources.map((r) => (
<ResourceItem key={`${r.provider}/${r.providerId}`} resource={r}></ResourceItem>
))}
</tbody>
</table>
</div>
<Pagination
timestamp={props.timestamp}
page={props.page}
link={props.link}
navigate={props.navigate}
complete={props.complete ?? false}
/>
</div>
);
}
Expand Down Expand Up @@ -131,8 +142,7 @@ export const ResourceItem = memo((props: { resource: Resource<{ tracker: true }>
/>
</a> */}
<span className="text-xs text-zinc-400">
发布于{' '}
{formatInTimeZone(new Date(r.createdAt), 'Asia/Shanghai', 'yyyy-MM-dd HH:mm')}
发布于 {formatChinaTime(new Date(r.createdAt))}
</span>
{/* <span className="text-xs text-zinc-400">上传者 {r.publisher.name}</span>
{r.fansub && <span className="text-xs text-zinc-400">字幕组 {r.fansub?.name}</span>} */}
Expand Down
5 changes: 5 additions & 0 deletions apps/frontend/web/app/components/Resources/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { formatInTimeZone } from 'date-fns-tz';

export function formatChinaTime(date: Date) {
return formatInTimeZone(date, 'Asia/Shanghai', 'yyyy-MM-dd HH:mm');
}
17 changes: 14 additions & 3 deletions apps/frontend/web/app/routes/_index/route.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { Resource } from 'animegarden';

import { useLoaderData } from '@remix-run/react';
import { useLoaderData, useNavigate } from '@remix-run/react';
import { type LoaderFunctionArgs, type MetaFunction, json } from '@remix-run/cloudflare';

import Layout from '~/layouts/Layout';
import Resources from '~/components/Resources';
import { fetchResources } from '~/utils';

import Resources from '@/components/Resources';
import { Error } from '../resources.($page)/Error';

export const loader = async ({ params }: LoaderFunctionArgs) => {
const { ok, resources, timestamp } = await fetchResources({
Expand All @@ -30,7 +31,17 @@ export default function Index() {
return (
<Layout>
<div className="w-full pt-12 pb-24">
<Resources resources={resources}></Resources>
{ok ? (
<Resources
resources={resources}
page={1}
timestamp={new Date(timestamp!)}
complete={false}
link={(page) => `/resources/${page}?type=動畫`}
></Resources>
) : (
<Error></Error>
)}
</div>
</Layout>
);
Expand Down
45 changes: 39 additions & 6 deletions apps/frontend/web/app/routes/resources.($page)/route.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useLoaderData } from '@remix-run/react';
import { redirect, useLoaderData, useLocation } from '@remix-run/react';
import { json, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/cloudflare';

import { parseSearchURL, Resource } from 'animegarden';
Expand All @@ -7,6 +7,7 @@ import Layout from '@/layouts/Layout';
import Resources from '@/components/Resources';
import { fetchResources } from '@/utils';

import { Error } from './Error';
import { Filter } from './Filter';

export const meta: MetaFunction = () => {
Expand All @@ -18,23 +19,55 @@ export const meta: MetaFunction = () => {

export const loader = async ({ request, params }: LoaderFunctionArgs) => {
const url = new URL(request.url);

// Redirect to the first page
if (params.page === undefined) {
if (url.pathname.endsWith('/')) {
url.pathname += '1';
} else {
url.pathname += '/1';
}
return redirect(url.toString());
}

const parsed = parseSearchURL(url.searchParams, { pageSize: 80 });
const { ok, resources, filter, timestamp } = await fetchResources({
const page = +(params.page ?? '1');
const { ok, resources, complete, filter, timestamp } = await fetchResources({
...parsed,
page: +(params.page ?? '1')
});

return json({ ok, resources: resources as Resource<{ tracker: true }>[], filter, timestamp });
return json({
ok,
resources: resources as Resource<{ tracker: true }>[],
complete,
page,
filter,
timestamp
});
};

export default function ResourcesIndex() {
const { ok, resources, filter, timestamp } = useLoaderData<typeof loader>();
const location = useLocation();
const { ok, resources, complete, filter, page, timestamp } = useLoaderData<typeof loader>();

return (
<Layout>
<div className="w-full pt-12 pb-24">
<Filter filter={filter as any}></Filter>
<Resources resources={resources}></Resources>
{ok ? (
<>
<Filter filter={filter as any}></Filter>
<Resources
resources={resources}
page={page}
complete={complete}
timestamp={new Date(timestamp!)}
link={(page) => `/resources/${page}${location.search}`}
></Resources>
</>
) : (
<Error></Error>
)}
</div>
</Layout>
);
Expand Down
25 changes: 19 additions & 6 deletions apps/frontend/web/app/utils/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,27 @@ export async function fetchResources(
options: {
fetch?: typeof ofetch;
signal?: AbortSignal;
retry?: number;
} = {}
) {
return await rawFetchResources(options.fetch ?? ofetch, {
baseURL,
...filter,
signal: options.signal,
tracker: true
});
try {
return await rawFetchResources(options.fetch ?? ofetch, {
baseURL,
tracker: true,
signal: options.signal,
retry: options.retry,
...filter
});
} catch (error) {
console.error(error);
return {
ok: false,
resources: [],
complete: false,
filter: undefined,
timestamp: undefined
};
}
}

export async function fetchResourceDetail(provider: string, href: string) {
Expand Down
1 change: 1 addition & 0 deletions apps/frontend/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@remix-run/server-runtime": "^2.12.0",
"animegarden": "workspace:*",
"anitomy": "0.0.35",
"clsx": "^2.1.1",
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.1",
"hono": "^4.6.1",
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 19d2ef3

Please sign in to comment.