feat: Pagination + usePagination hook

This commit is contained in:
LynithDev 2024-09-01 19:54:43 +02:00
parent c31f1086c7
commit a6f65bb5fb
3 changed files with 152 additions and 73 deletions

View file

@ -11,7 +11,7 @@
@apply opacity-0 transition-opacity;
@apply absolute left-1/2 -translate-x-1/2 top-0 -translate-y-6/5;
@apply w-max max-w-52;
@apply backdrop-blur-lg rounded-md bg-black/50 p-2 text-center text-sm text-wrap text-white shadow shadow-black/30;
@apply backdrop-blur-lg rounded-md bg-black/50 p-2 text-center text-sm text-wrap line-height-relaxed text-white shadow shadow-black/30;
}
}

View file

@ -0,0 +1,104 @@
import { ChevronLeftIcon, ChevronRightIcon } from '@untitled-theme/icons-solid';
import { For, Show, createSignal } from 'solid-js';
import Button from '~ui/components/base/Button';
interface PaginationOptions {
itemsCount: () => number;
itemsPerPage: () => number;
initialPage?: () => number;
};
function usePagination(options: PaginationOptions) {
const [page, setPage] = createSignal(Math.max(1, options.initialPage?.() || 1));
const next = () => setPage(page => page + 1);
const prev = () => setPage(page => page - 1);
const totalPages = () => Math.ceil(options.itemsCount() / options.itemsPerPage());
const hasNext = () => page() < totalPages();
const hasPrev = () => page() > 1;
const offset = () => (page() - 1) * options.itemsPerPage();
// Navigation
function getVisiblePageNumbers(page: number) {
const pages: number[] = [];
const left = page - 1 > 1 ? page - 1 : null;
const right = page + 1 < totalPages() ? page + 1 : null;
if (left !== null)
pages.push(left);
if (page !== 1 && page !== totalPages())
pages.push(page);
if (right !== null)
pages.push(right);
return pages;
}
function PaginationBtn(props: { page: number }) {
return (
<Button
buttonStyle={props.page === page() ? 'iconPrimary' : 'iconSecondary'}
children={props.page}
onClick={() => {
if (props.page === page())
return;
setPage(props.page);
}}
/>
);
}
const Navigation = () => (
<div class="flex flex-row items-center justify-center gap-x-1">
<Button
buttonStyle="iconSecondary"
children={<ChevronLeftIcon />}
disabled={page() <= 1}
onClick={() => setPage(page => page - 1)}
/>
<PaginationBtn page={1} />
<Show when={page() > 1 + 2}>
<span>...</span>
</Show>
<For each={getVisiblePageNumbers(page())}>
{page => <PaginationBtn page={page} />}
</For>
<Show when={page() < totalPages() - 2}>
<span>...</span>
</Show>
<PaginationBtn page={totalPages()} />
<Button
buttonStyle="iconSecondary"
children={<ChevronRightIcon />}
disabled={page() >= totalPages()}
onClick={() => setPage(page => page + 1)}
/>
</div>
);
return {
page,
next,
prev,
hasNext,
hasPrev,
totalPages,
offset,
Navigation,
};
}
export default usePagination;

View file

@ -1,6 +1,6 @@
import { For, type ParentProps, Show, createContext, createEffect, createSignal, on, useContext } from 'solid-js';
import { For, Match, type ParentProps, Show, Switch, createContext, createEffect, createSignal, on, useContext } from 'solid-js';
import { A, type Params, Route, useSearchParams } from '@solidjs/router';
import { CalendarIcon, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ClockRewindIcon, Download01Icon, File02Icon, HeartIcon, LinkExternal01Icon } from '@untitled-theme/icons-solid';
import { CalendarIcon, ChevronDownIcon, ClockRewindIcon, Download01Icon, File02Icon, HeartIcon, LinkExternal01Icon } from '@untitled-theme/icons-solid';
import type { Cluster, ManagedPackage, ManagedUser, ManagedVersion, Providers } from '@onelauncher/client/bindings';
import { getLicenseUrl, getPackageUrl, upperFirst } from '../../../utils';
import { bridge } from '~imports';
@ -14,6 +14,7 @@ import useBrowser from '~ui/hooks/useBrowser';
import SteveHead from '~assets/images/steve.png';
import usePromptOpener from '~ui/hooks/usePromptOpener';
import Link from '~ui/components/base/Link';
import usePagination from '~ui/hooks/usePagination';
interface BrowserModParams extends Params {
id: string;
@ -357,10 +358,13 @@ function BrowserPackageBody() {
function BrowserPackageVersions() {
const context = useContext(BrowserPackageContext);
const [page, setPage] = createSignal(1);
const MAX_ITEMS_PER_PAGE = 20;
const { page, Navigation } = usePagination({
itemsCount: () => context?.versions.length || 0,
itemsPerPage: () => MAX_ITEMS_PER_PAGE,
});
const getVersionsForPage = (page: number) => {
if (context === null)
return [];
@ -372,15 +376,6 @@ function BrowserPackageVersions() {
return newestToOldest.slice(start, end);
};
const getMaxPage = () => {
if (context === null)
return 1;
return Math.ceil(context.versions.length / MAX_ITEMS_PER_PAGE);
};
const diffFrom = (from: number = page()) => getMaxPage() - from;
const [versions, { refetch }] = useCommand(context, async () => {
if (context === null)
return { error: 'No package context', status: 'error' };
@ -390,35 +385,24 @@ function BrowserPackageVersions() {
return await bridge.commands.getProviderPackageVersions(context.provider, list);
});
function PaginationBtn(props: { page: number }) {
return (
<Button
buttonStyle={props.page === page() ? 'iconPrimary' : 'iconSecondary'}
children={props.page}
onClick={() => {
if (props.page === page())
return;
setPage(props.page);
}}
/>
);
}
let container!: HTMLDivElement;
createEffect(on(() => page(), () => {
refetch();
container.scrollIntoView({ behavior: 'smooth' });
container.parentElement?.scrollIntoView({ behavior: 'smooth' });
}));
return (
<div ref={container} class="w-full flex flex-1 flex-col gap-y-1">
<h1>
Versions - Page
{' '}
{page()}
</h1>
<div class="flex flex-row justify-between">
<h1>
Versions - Page
{' '}
{page()}
</h1>
<Navigation />
</div>
<table class="w-full border-separate border-spacing-x-none border-spacing-y-1">
<thead>
@ -441,49 +425,28 @@ function BrowserPackageVersions() {
</tbody>
</table>
<div class="flex flex-row gap-x-1">
<Button
buttonStyle="iconSecondary"
children={<ChevronLeftIcon />}
disabled={page() <= 1}
onClick={() => setPage(page => page - 1)}
/>
<PaginationBtn page={1} />
{/* TODO: Pagination */}
{/* <Switch>
<Match when={}
</Switch> */}
<Button
buttonStyle="iconSecondary"
children={<ChevronRightIcon />}
disabled={page() >= getMaxPage()}
onClick={() => setPage(page => page + 1)}
/>
</div>
<Navigation />
</div>
);
}
function VersionRow(props: ManagedVersion) {
function colorForType(type: string) {
switch (type) {
case 'release':
return 'bg-code-trace';
case 'snapshot':
return 'bg-code-debug';
case 'beta':
return 'bg-code-warn';
case 'alpha':
return 'bg-code-error';
default:
return 'bg-gray-05';
}
function colorForType(type: string) {
switch (type) {
case 'release':
return 'bg-code-trace';
case 'snapshot':
return 'bg-code-debug';
case 'beta':
return 'bg-code-warn';
case 'alpha':
return 'bg-code-error';
default:
return 'bg-gray-05';
}
}
function VersionRow(props: ManagedVersion) {
return (
<tr class="my-2 bg-page-elevated px-4 [&>td]:py-4">
<td class="rounded-l-lg px-4">
@ -502,7 +465,19 @@ function VersionRow(props: ManagedVersion) {
</td>
<td>
{props.game_versions.join(', ')}
<Switch>
<Match when={props.game_versions.length > 3}>
<Tooltip text={props.game_versions.join(', ')}>
<span>
{props.game_versions.slice(-3).toReversed().join(', ')}
...
</span>
</Tooltip>
</Match>
<Match when>
<span>{props.game_versions.join(', ')}</span>
</Match>
</Switch>
</td>
<td>
@ -514,7 +489,7 @@ function VersionRow(props: ManagedVersion) {
</td>
<td>
{props.downloads}
{props.downloads.toLocaleString()}
</td>
<td class="rounded-r-lg">