diff --git a/src/app/(pages)/player/[...slug]/page.tsx b/src/app/(pages)/player/[...slug]/page.tsx index ce830bb..6259b0f 100644 --- a/src/app/(pages)/player/[...slug]/page.tsx +++ b/src/app/(pages)/player/[...slug]/page.tsx @@ -10,6 +10,9 @@ type Props = { params: Promise<{ slug: string[]; }>; + searchParams: Promise<{ + [key: string]: string | undefined; + }>; }; export async function generateMetadata({ params }: Props): Promise { @@ -40,11 +43,13 @@ export async function generateMetadata({ params }: Props): Promise { }; } -export default async function Search({ params }: Props) { +export default async function Search({ params, searchParams }: Props) { const { slug } = await params; + const searchParamss = await searchParams; const id = slug[0]; // The players id const sort: ScoreSort = (slug[1] as ScoreSort) || "recent"; // The sorting method const page = parseInt(slug[2]) || 1; // The page number + const search = searchParamss["search"] || ""; // The search query const response = await scoresaberService.lookupPlayer(id, false); if (response == undefined) { // Invalid player id @@ -55,6 +60,7 @@ export default async function Search({ params }: Props) { playerId: id, sort, page, + search, }); const { player } = response; return ( @@ -62,6 +68,7 @@ export default async function Search({ params }: Props) { diff --git a/src/components/player/player-data.tsx b/src/components/player/player-data.tsx index da0eae8..60a0e16 100644 --- a/src/components/player/player-data.tsx +++ b/src/components/player/player-data.tsx @@ -17,6 +17,7 @@ const REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes type Props = { initialPlayerData: ScoreSaberPlayer; initialScoreData?: ScoreSaberPlayerScoresPageToken; + initialSearch?: string; sort: ScoreSort; page: number; }; @@ -24,6 +25,7 @@ type Props = { export default function PlayerData({ initialPlayerData: initalPlayerData, initialScoreData, + initialSearch, sort, page, }: Props) { @@ -50,6 +52,7 @@ export default function PlayerData({ )} ({ - page: page, - sort: sort, - }); + const [pageState, setPageState] = useState({ page, sort }); const [previousPage, setPreviousPage] = useState(page); const [currentScores, setCurrentScores] = useState< ScoreSaberPlayerScoresPageToken | undefined >(initialScoreData); + const [searchState, setSearchState] = useState({ + query: initialSearch || "", + }); + const [debouncedSearchTerm, setDebouncedSearchTerm] = useState( + initialSearch || "", + ); + + const isSearchActive = debouncedSearchTerm.length >= 3; + + // Debounce the search query + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedSearchTerm(searchState.query); + }, INPUT_DEBOUNCE_DELAY); + + return () => clearTimeout(handler); + }, [searchState.query]); const { data: scores, @@ -96,26 +88,25 @@ export default function PlayerScores({ isLoading, refetch, } = useQuery({ - queryKey: ["playerScores", player.id, pageState], - queryFn: () => - scoresaberService.lookupPlayerScores({ + queryKey: ["playerScores", player.id, pageState, debouncedSearchTerm], + queryFn: () => { + return scoresaberService.lookupPlayerScores({ playerId: player.id, - sort: pageState.sort, page: pageState.page, - }), - staleTime: 30 * 1000, // Cache data for 30 seconds + sort: pageState.sort, + ...(isSearchActive && { search: debouncedSearchTerm }), + }); + }, + staleTime: 30 * 1000, }); const handleScoreLoad = useCallback(async () => { - setFirstLoad(false); - if (!firstLoad) { - await controls.start( - previousPage >= pageState.page ? "hiddenRight" : "hiddenLeft", - ); - } + await controls.start( + previousPage >= pageState.page ? "hiddenRight" : "hiddenLeft", + ); setCurrentScores(scores); await controls.start("visible"); - }, [scores, controls, previousPage, firstLoad, pageState.page]); + }, [scores, controls, previousPage, pageState.page]); const handleSortChange = (newSort: ScoreSort) => { if (newSort !== pageState.sort) { @@ -124,31 +115,44 @@ export default function PlayerScores({ }; useEffect(() => { - if (scores) { - handleScoreLoad(); - } - }, [scores, isError, handleScoreLoad]); + if (scores) handleScoreLoad(); + }, [scores, handleScoreLoad]); useEffect(() => { - const newUrl = `/player/${player.id}/${pageState.sort}/${pageState.page}`; + const newUrl = `/player/${player.id}/${pageState.sort}/${pageState.page}${isSearchActive ? `?search=${debouncedSearchTerm}` : ""}`; window.history.replaceState( { ...window.history.state, as: newUrl, url: newUrl }, "", newUrl, ); + }, [pageState, debouncedSearchTerm, player.id]); + + useEffect(() => { refetch(); - }, [pageState, refetch, player.id]); + }, [pageState, debouncedSearchTerm, refetch]); + + const handleSearchChange = (query: string) => { + setSearchState({ query }); + }; + + const clearSearch = () => { + setSearchState({ query: "" }); + setDebouncedSearchTerm(""); // Clear the debounced term + }; + + const invalidSearch = + searchState.query.length >= 1 && searchState.query.length < 3; return ( -
-
- {Object.values(scoreSort).map((sortOption, index) => ( +
+
+ {scoreSort.map((sortOption) => (
- {/* todo: add search */} - {/**/} +
+ handleSearchChange(e.target.value)} + /> + {searchState.query && ( // Show clear button only if there's a query + + )} +
{currentScores && ( <>
- {isError &&

Oopsies! Something went wrong.

} - {currentScores.playerScores.length === 0 && ( -

No scores found. Invalid Page?

- )} + {isError || + (currentScores.playerScores.length === 0 && ( +

No scores found. Invalid Page or Search?

+ ))}
{ + onPageChange={(newPage) => { setPreviousPage(pageState.page); - setPageState({ page, sort: pageState.sort }); + setPageState({ ...pageState, page: newPage }); }} /> diff --git a/src/components/score/leaderboard-button.tsx b/src/components/score/leaderboard-button.tsx index 00af485..fccaf74 100644 --- a/src/components/score/leaderboard-button.tsx +++ b/src/components/score/leaderboard-button.tsx @@ -13,7 +13,7 @@ export default function LeaderboardButton({ setIsLeaderboardExpanded, }: Props) { return ( -
+