From 9d1cb1bc92eeb5423c4e8f39c425d08a5faa4187 Mon Sep 17 00:00:00 2001 From: majian Date: Sun, 28 Apr 2024 13:52:45 +0800 Subject: [PATCH] improvement: Optimizing the experience of the app list page (#3885) --- web/app/(commonLayout)/apps/Apps.tsx | 21 +++++--- .../apps/hooks/useAppsQueryState.ts | 53 +++++++++++++++++++ 2 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 web/app/(commonLayout)/apps/hooks/useAppsQueryState.ts diff --git a/web/app/(commonLayout)/apps/Apps.tsx b/web/app/(commonLayout)/apps/Apps.tsx index 1ec34cf6b..744bb9c9d 100644 --- a/web/app/(commonLayout)/apps/Apps.tsx +++ b/web/app/(commonLayout)/apps/Apps.tsx @@ -1,11 +1,12 @@ 'use client' -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import useSWRInfinite from 'swr/infinite' import { useTranslation } from 'react-i18next' import { useDebounceFn } from 'ahooks' import AppCard from './AppCard' import NewAppCard from './NewAppCard' +import useAppsQueryState from './hooks/useAppsQueryState' import type { AppListResponse } from '@/models/app' import { fetchAppList } from '@/service/apps' import { useAppContext } from '@/context/app-context' @@ -54,10 +55,15 @@ const Apps = () => { const [activeTab, setActiveTab] = useTabSearchParams({ defaultTab: 'all', }) - const [tagFilterValue, setTagFilterValue] = useState([]) - const [tagIDs, setTagIDs] = useState([]) - const [keywords, setKeywords] = useState('') - const [searchKeywords, setSearchKeywords] = useState('') + const { query: { tagIDs = [], keywords = '' }, setQuery } = useAppsQueryState() + const [tagFilterValue, setTagFilterValue] = useState(tagIDs) + const [searchKeywords, setSearchKeywords] = useState(keywords) + const setKeywords = useCallback((keywords: string) => { + setQuery(prev => ({ ...prev, keywords })) + }, [setQuery]) + const setTagIDs = useCallback((tagIDs: string[]) => { + setQuery(prev => ({ ...prev, tagIDs })) + }, [setQuery]) const { data, isLoading, setSize, mutate } = useSWRInfinite( (pageIndex: number, previousPageData: AppListResponse) => getKey(pageIndex, previousPageData, activeTab, tagIDs, searchKeywords), @@ -81,17 +87,18 @@ const Apps = () => { } }, []) + const hasMore = data?.at(-1)?.has_more ?? true useEffect(() => { let observer: IntersectionObserver | undefined if (anchorRef.current) { observer = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting && !isLoading) + if (entries[0].isIntersecting && !isLoading && hasMore) setSize((size: number) => size + 1) }, { rootMargin: '100px' }) observer.observe(anchorRef.current) } return () => observer?.disconnect() - }, [isLoading, setSize, anchorRef, mutate]) + }, [isLoading, setSize, anchorRef, mutate, hasMore]) const { run: handleSearch } = useDebounceFn(() => { setSearchKeywords(keywords) diff --git a/web/app/(commonLayout)/apps/hooks/useAppsQueryState.ts b/web/app/(commonLayout)/apps/hooks/useAppsQueryState.ts new file mode 100644 index 000000000..fae5357bf --- /dev/null +++ b/web/app/(commonLayout)/apps/hooks/useAppsQueryState.ts @@ -0,0 +1,53 @@ +import { type ReadonlyURLSearchParams, usePathname, useRouter, useSearchParams } from 'next/navigation' +import { useCallback, useEffect, useMemo, useState } from 'react' + +type AppsQuery = { + tagIDs?: string[] + keywords?: string +} + +// Parse the query parameters from the URL search string. +function parseParams(params: ReadonlyURLSearchParams): AppsQuery { + const tagIDs = params.get('tagIDs')?.split(';') + const keywords = params.get('keywords') || undefined + return { tagIDs, keywords } +} + +// Update the URL search string with the given query parameters. +function updateSearchParams(query: AppsQuery, current: URLSearchParams) { + const { tagIDs, keywords } = query || {} + + if (tagIDs && tagIDs.length > 0) + current.set('tagIDs', tagIDs.join(';')) + else + current.delete('tagIDs') + + if (keywords) + current.set('keywords', keywords) + else + current.delete('keywords') +} + +function useAppsQueryState() { + const searchParams = useSearchParams() + const [query, setQuery] = useState(() => parseParams(searchParams)) + + const router = useRouter() + const pathname = usePathname() + const syncSearchParams = useCallback((params: URLSearchParams) => { + const search = params.toString() + const query = search ? `?${search}` : '' + router.push(`${pathname}${query}`) + }, [router, pathname]) + + // Update the URL search string whenever the query changes. + useEffect(() => { + const params = new URLSearchParams(searchParams) + updateSearchParams(query, params) + syncSearchParams(params) + }, [query, searchParams, syncSearchParams]) + + return useMemo(() => ({ query, setQuery }), [query]) +} + +export default useAppsQueryState