From 5e9b93bea2aaf54a3e1d92d44099723142dff39e Mon Sep 17 00:00:00 2001 From: Szedann <65607498+Szedann@users.noreply.github.com> Date: Sun, 20 Jul 2025 13:26:31 +0200 Subject: [PATCH] feat: search pagination --- .../frontend/src/hooks/usePagination.tsx | 118 +++++++++++++++ .../app/browser/package.$provider.$slug.tsx | 141 +++++++++--------- .../src/routes/app/browser/search.tsx | 37 ++++- .../frontend/src/styles/global.css | 6 +- 4 files changed, 231 insertions(+), 71 deletions(-) create mode 100644 apps/onelauncher/frontend/src/hooks/usePagination.tsx diff --git a/apps/onelauncher/frontend/src/hooks/usePagination.tsx b/apps/onelauncher/frontend/src/hooks/usePagination.tsx new file mode 100644 index 00000000..33d5937e --- /dev/null +++ b/apps/onelauncher/frontend/src/hooks/usePagination.tsx @@ -0,0 +1,118 @@ +import { Button, Show } from '@onelauncher/common/components'; +import { ChevronRightIcon, ChevronLeftIcon } from '@untitled-theme/icons-react'; +import { useState } from 'react'; + +export interface PaginationOptions { + itemsCount: number; + itemsPerPage: number; + initialPage?: number; +}; + +function usePagination(options: PaginationOptions) { + // const [options, setOptions] = useState(initialOptions); + + const [page, setPage] = useState(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; + + // Exported + function reset(/*options: PaginationOptions*/) { + setPage(1); + } + + 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 ( + + ); + } + + const Navigation = () => ( +
+ +
+ ); + + return { + page, + next, + prev, + hasNext, + hasPrev, + totalPages, + offset, + reset, + options, + Navigation, + }; +} + +export default usePagination; diff --git a/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx b/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx index ffb75066..e3d3f84b 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/package.$provider.$slug.tsx @@ -13,16 +13,16 @@ import { Collection, ListBox, ListBoxItem, Popover, Select } from 'react-aria-co export const Route = createFileRoute('/app/browser/package/$provider/$slug')({ - component: RouteComponent, + component: RouteComponent, }) -function includes(list:{includes: (arg0:C)=>boolean}, element:T): element is C{ +function includes(list: { includes: (arg0: C) => boolean }, element: T): element is C { return list.includes(element as unknown as C) } type PackageContextType = { - pkg: ManagedPackage|undefined - versions: Paginated|undefined + pkg: ManagedPackage | undefined + versions: Paginated | undefined } const PackageContext = createContext({ @@ -31,25 +31,26 @@ const PackageContext = createContext({ }) function RouteComponent() { - const {provider, slug} = Route.useParams() - if(!includes(PROVIDERS, provider)) throw new Error("Invalid provider"); + const { provider, slug } = Route.useParams() + if (!includes(PROVIDERS, provider)) throw new Error("Invalid provider"); const packageData = usePackageData(provider, slug) const browserContext = useBrowserContext() - const {data: versions} = usePackageVersions(provider, slug, { - mc_versions: browserContext.cluster ? [browserContext.cluster.mc_version] : null, - loaders: browserContext.cluster ? [browserContext.cluster.mc_loader] : null, - limit: 50}) + const { data: versions } = usePackageVersions(provider, slug, { + mc_versions: browserContext.cluster ? [browserContext.cluster.mc_version] : null, + loaders: browserContext.cluster ? [browserContext.cluster.mc_loader] : null, + limit: 50 + }) return ( - +
- About - Versions + About + Versions
@@ -65,13 +66,13 @@ function RouteComponent() { } -function BrowserSidebar({package: pkg}: { package: ManagedPackage }) { - const {provider} = Route.useParams() +function BrowserSidebar({ package: pkg }: { package: ManagedPackage }) { + const { provider } = Route.useParams() const createdAt = useMemo(() => pkg.created ? new Date(pkg.created) : null, []); const updatedAt = useMemo(() => pkg.updated ? new Date(pkg.updated) : null, []); - const authors = useCommand("getUsersFromAuthor", ()=>bindings.core.getUsersFromAuthor(provider as Provider, pkg.author)) + const authors = useCommand("getUsersFromAuthor", () => bindings.core.getUsersFromAuthor(provider as Provider, pkg.author)) return (
@@ -120,8 +121,8 @@ function BrowserSidebar({package: pkg}: { package: ManagedPackage }) {

Authors

{ authors.isSuccess - ? authors.data.map(author=>) - :

Loading...

+ ? authors.data.map(author => ) + :

Loading...

}
@@ -183,100 +184,104 @@ function colorForType(type: string) { } } -function InstallButton({...pkg}:ManagedPackage){ - const triggerRef = useRef(null) - const elementRef = useRef(null) - const {provider, slug} = Route.useParams() +function InstallButton({ ...pkg }: ManagedPackage) { + const triggerRef = useRef(null) + const { provider, slug } = Route.useParams() const [open, setOpen] = useState(false) const clusters = useClusters() const browserContext = useBrowserContext() - const {versions} = useContext(PackageContext) - const version = useMemo(()=>{ - if(!versions || !browserContext.cluster) return undefined; - return versions.items.findLast(version=> + const { versions } = useContext(PackageContext) + const version = useMemo(() => { + if (!versions || !browserContext.cluster) return undefined; + return versions.items.findLast(version => version.mc_versions.includes(browserContext.cluster!.mc_version) && version.loaders.includes(browserContext.cluster!.mc_loader) ) - },[browserContext.cluster, versions]) + }, [browserContext.cluster, versions]) - function download(){ - if(!version || !browserContext.cluster || !includes(PROVIDERS, provider)) return false; + function download() { + if (!version || !browserContext.cluster || !includes(PROVIDERS, provider)) return false; downloadPackage(browserContext.cluster, provider, version) } return ( - { + const cluster = clusters?.find(cluster => cluster.id as unknown as number == e) + if (cluster) browserContext.setCluster(cluster) + }}> +
+ + + + + {item => {item.name}} + + + +
- - - - {item=> {item.name}} - - - - + ) } -function Author({author}:{author:ManagedUser}){ +function Author({ author }: { author: ManagedUser }) { //TODO: onclick and fallback avatar url return ( - {if(author.url) openUrl(author.url)}} - > - {`${author.username}'s - + + ) } -function downloadPackage(cluster: ClusterModel, provider:Provider, version: ManagedVersion, skipCompatibility = false){ +function downloadPackage(cluster: ClusterModel, provider: Provider, version: ManagedVersion, skipCompatibility = false) { return bindings.core.downloadPackage(provider, version.project_id, version.version_id, cluster.id, skipCompatibility) } diff --git a/apps/onelauncher/frontend/src/routes/app/browser/search.tsx b/apps/onelauncher/frontend/src/routes/app/browser/search.tsx index d02f5caa..e31ffb6a 100644 --- a/apps/onelauncher/frontend/src/routes/app/browser/search.tsx +++ b/apps/onelauncher/frontend/src/routes/app/browser/search.tsx @@ -2,8 +2,11 @@ import { PackageGrid } from '@/components/content/PackageItem'; import { useBrowserContext, useBrowserSearch } from '@/hooks/useBrowser'; import { Show } from '@onelauncher/common/components'; import { createFileRoute } from '@tanstack/react-router'; -import { useEffect } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { BrowserLayout } from './route'; +import type { Paginated, SearchResult } from '@/bindings.gen'; +import type { PaginationOptions } from '@/hooks/usePagination'; +import usePagination from '@/hooks/usePagination'; export const Route = createFileRoute('/app/browser/search')({ component: RouteComponent, @@ -31,9 +34,39 @@ function Search() { <> { search.data - ? + ? :

Loading

} ); } + +function Results({results}:{results:Paginated}){ + const context = useBrowserContext(); + const [oldTotal, setOldTotal] = useState(results.total) + const pagination = usePagination({ + itemsCount: results.total as unknown as number, + itemsPerPage: context.query.limit as unknown as number + }) + useEffect(()=>{ + context.setQuery({...context.query, offset: pagination.offset as unknown as bigint}) + },[pagination.page]) + useEffect(()=>{ + if(oldTotal === results.total) return; + pagination.reset() + console.log("pagination reset", results.total) + setOldTotal(results.total) + },[results.total]) + return ( +
+
+ +
+ {results.items.length == 0 + ?

No results

+ : + } + +
+ ) +} diff --git a/apps/onelauncher/frontend/src/styles/global.css b/apps/onelauncher/frontend/src/styles/global.css index 06a0ee4a..33ed62d6 100644 --- a/apps/onelauncher/frontend/src/styles/global.css +++ b/apps/onelauncher/frontend/src/styles/global.css @@ -29,4 +29,8 @@ mask: linear-gradient(270deg, transparent, black 50%); background: linear-gradient(270deg, transparent, rgba(0, 0, 0, 0.85) 80%); -} \ No newline at end of file +} + +@utility text-trim{ + text-box: trim-both cap alphabetic +}