scroll to the top of the score on when new scores are loaded
Some checks failed
Deploy / deploy (push) Has been cancelled

This commit is contained in:
Lee 2024-09-30 22:39:51 +01:00
parent 732599a721
commit 08ce0cf5aa

View File

@ -3,7 +3,7 @@ import useWindowDimensions from "@/hooks/use-window-dimensions";
import { ClockIcon, TrophyIcon, XMarkIcon } from "@heroicons/react/24/solid"; import { ClockIcon, TrophyIcon, XMarkIcon } from "@heroicons/react/24/solid";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { motion, useAnimation, Variants } from "framer-motion"; import { motion, useAnimation, Variants } from "framer-motion";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import Card from "../card"; import Card from "../card";
import Pagination from "../input/pagination"; import Pagination from "../input/pagination";
import { Button } from "../ui/button"; import { Button } from "../ui/button";
@ -57,10 +57,10 @@ export default function PlayerScores({ initialScoreData, initialSearch, player,
const [currentScores, setCurrentScores] = useState<ScoreSaberPlayerScoresPageToken | undefined>(initialScoreData); const [currentScores, setCurrentScores] = useState<ScoreSaberPlayerScoresPageToken | undefined>(initialScoreData);
const [searchTerm, setSearchTerm] = useState(initialSearch || ""); const [searchTerm, setSearchTerm] = useState(initialSearch || "");
const debouncedSearchTerm = useDebounce(searchTerm, 250); const debouncedSearchTerm = useDebounce(searchTerm, 250);
const [shouldFetch, setShouldFetch] = useState(false); // New state to control fetching
const topOfScoresRef = useRef(null);
const isSearchActive = debouncedSearchTerm.length >= 3; const isSearchActive = debouncedSearchTerm.length >= 3;
const [shouldFetch, setShouldFetch] = useState(false); // New state to control fetching
const { const {
data: scores, data: scores,
isError, isError,
@ -79,12 +79,20 @@ export default function PlayerScores({ initialScoreData, initialSearch, player,
enabled: shouldFetch && (debouncedSearchTerm.length >= 3 || debouncedSearchTerm.length === 0), // Only enable if we set shouldFetch to true enabled: shouldFetch && (debouncedSearchTerm.length >= 3 || debouncedSearchTerm.length === 0), // Only enable if we set shouldFetch to true
}); });
const handleScoreLoad = useCallback(async () => { /**
* Starts the animation for the scores.
*/
const handleScoreAnimation = useCallback(async () => {
await controls.start(previousPage >= pageState.page ? "hiddenRight" : "hiddenLeft"); await controls.start(previousPage >= pageState.page ? "hiddenRight" : "hiddenLeft");
setCurrentScores(scores); setCurrentScores(scores);
await controls.start("visible"); await controls.start("visible");
}, [scores, controls, previousPage, pageState.page]); }, [scores, controls, previousPage, pageState.page]);
/**
* Change the score sort.
*
* @param newSort the new sort
*/
const handleSortChange = (newSort: ScoreSort) => { const handleSortChange = (newSort: ScoreSort) => {
if (newSort !== pageState.sort) { if (newSort !== pageState.sort) {
setPageState({ page: 1, sort: newSort }); setPageState({ page: 1, sort: newSort });
@ -92,15 +100,11 @@ export default function PlayerScores({ initialScoreData, initialSearch, player,
} }
}; };
useEffect(() => { /**
if (scores) handleScoreLoad(); * Change the score search term.
}, [scores, handleScoreLoad]); *
* @param query the new search term
useEffect(() => { */
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, isSearchActive]);
const handleSearchChange = (query: string) => { const handleSearchChange = (query: string) => {
setSearchTerm(query); setSearchTerm(query);
if (query.length >= 3) { if (query.length >= 3) {
@ -110,14 +114,50 @@ export default function PlayerScores({ initialScoreData, initialSearch, player,
} }
}; };
/**
* Clears the score search term.
*/
const clearSearch = () => { const clearSearch = () => {
setSearchTerm(""); setSearchTerm("");
}; };
/**
* Handle score animation.
*/
useEffect(() => {
if (scores) handleScoreAnimation();
}, [scores, handleScoreAnimation]);
/**
* Handle updating the URL when the page number,
* sort, or search term changes.
*/
useEffect(() => {
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, isSearchActive]);
/**
* Handle scrolling to the top of the
* scores when new scores are loaded.
*/
useEffect(() => {
if (topOfScoresRef.current) {
const topOfScoresPosition = (topOfScoresRef.current as any).getBoundingClientRect().top + window.scrollY;
window.scrollTo({
top: topOfScoresPosition - 55, // Navbar height (plus some padding)
behavior: "smooth",
});
}
}, [pageState, topOfScoresRef]);
const invalidSearch = searchTerm.length >= 1 && searchTerm.length < 3; const invalidSearch = searchTerm.length >= 1 && searchTerm.length < 3;
return ( return (
<Card className="flex gap-1"> <Card className="flex gap-1">
<div className="flex flex-col items-center w-full gap-2 relative"> <div className="flex flex-col items-center w-full gap-2 relative">
{/* Where to scroll to when new scores are loaded */}
<div ref={topOfScoresRef} className="absolute flex h-11 p-11" />
<div className="flex gap-2"> <div className="flex gap-2">
{scoreSort.map(sortOption => ( {scoreSort.map(sortOption => (
<Button <Button