diff --git a/src/components/leaderboard/leaderboard-player.tsx b/src/components/leaderboard/leaderboard-player.tsx index cef4262..1e59718 100644 --- a/src/components/leaderboard/leaderboard-player.tsx +++ b/src/components/leaderboard/leaderboard-player.tsx @@ -13,7 +13,7 @@ export default function LeaderboardPlayer({ score }: Props) {
Song Artwork - - - +
+
+ + + +
); } diff --git a/src/components/leaderboard/leaderboard-scores.tsx b/src/components/leaderboard/leaderboard-scores.tsx index 5a1d73c..4df1261 100644 --- a/src/components/leaderboard/leaderboard-scores.tsx +++ b/src/components/leaderboard/leaderboard-scores.tsx @@ -5,11 +5,12 @@ import ScoreSaberLeaderboardToken from "@/common/model/token/scoresaber/score-sa import ScoreSaberLeaderboardScoresPageToken from "@/common/model/token/scoresaber/score-saber-leaderboard-scores-page-token"; import useWindowDimensions from "@/hooks/use-window-dimensions"; import { useQuery } from "@tanstack/react-query"; -import { motion } from "framer-motion"; -import { useEffect, useState } from "react"; +import { motion, useAnimation, Variants } from "framer-motion"; +import { useCallback, useEffect, useRef, useState } from "react"; import Card from "../card"; import Pagination from "../input/pagination"; import LeaderboardScore from "./leaderboard-score"; +import { scoreAnimation } from "@/components/score/score-animation"; type Props = { leaderboard: ScoreSaberLeaderboardToken; @@ -17,9 +18,12 @@ type Props = { export default function LeaderboardScores({ leaderboard }: Props) { const { width } = useWindowDimensions(); + const controls = useAnimation(); + const [previousPage, setPreviousPage] = useState(1); const [currentPage, setCurrentPage] = useState(1); const [currentScores, setCurrentScores] = useState(); + const topOfScoresRef = useRef(null); const { data: scores, @@ -32,16 +36,45 @@ export default function LeaderboardScores({ leaderboard }: Props) { staleTime: 30 * 1000, // Cache data for 30 seconds }); + /** + * Starts the animation for the scores. + */ + const handleScoreAnimation = useCallback(async () => { + await controls.start(previousPage >= currentPage ? "hiddenRight" : "hiddenLeft"); + setCurrentScores(scores); + await controls.start("visible"); + }, [controls, currentPage, previousPage, scores]); + + /** + * Set the current scores. + */ useEffect(() => { if (scores) { - setCurrentScores(scores); + handleScoreAnimation(); } - }, [scores]); + }, [scores, handleScoreAnimation]); + /** + * Handle page change. + */ useEffect(() => { refetch(); }, [leaderboard, currentPage, refetch]); + /** + * Handle scrolling to the top of the + * scores when new scores are loaded. + */ + useEffect(() => { + if (topOfScoresRef.current) { + const topOfScoresPosition = topOfScoresRef.current.getBoundingClientRect().top + window.scrollY; + window.scrollTo({ + top: topOfScoresPosition - 75, // Navbar height (plus some padding) + behavior: "smooth", + }); + } + }, [currentPage, topOfScoresRef]); + if (currentScores === undefined) { return undefined; } @@ -49,23 +82,36 @@ export default function LeaderboardScores({ leaderboard }: Props) { return ( + {/* Where to scroll to when new scores are loaded */} +
+
{isError &&

Oopsies! Something went wrong.

} {currentScores.scores.length === 0 &&

No scores found. Invalid Page?

}
-
+ {currentScores.scores.map((playerScore, index) => ( - + + + ))} -
+ { + setCurrentPage(newPage); + setPreviousPage(currentPage); + }} /> diff --git a/src/components/player/player-scores.tsx b/src/components/player/player-scores.tsx index 2a8f679..45a63e2 100644 --- a/src/components/player/player-scores.tsx +++ b/src/components/player/player-scores.tsx @@ -15,6 +15,7 @@ import { scoresaberService } from "@/common/service/impl/scoresaber"; import { Input } from "@/components/ui/input"; import { clsx } from "clsx"; import { useDebounce } from "@uidotdev/usehooks"; +import { scoreAnimation } from "@/components/score/score-animation"; type Props = { initialScoreData?: ScoreSaberPlayerScoresPageToken; @@ -42,12 +43,6 @@ const scoreSort = [ }, ]; -const scoreAnimation: Variants = { - hiddenRight: { opacity: 0, x: 50 }, - hiddenLeft: { opacity: 0, x: -50 }, - visible: { opacity: 1, x: 0, transition: { staggerChildren: 0.03 } }, -}; - export default function PlayerScores({ initialScoreData, initialSearch, player, sort, page }: Props) { const { width } = useWindowDimensions(); const controls = useAnimation(); @@ -156,7 +151,7 @@ export default function PlayerScores({ initialScoreData, initialSearch, player,
{/* Where to scroll to when new scores are loaded */} -
+
{scoreSort.map(sortOption => ( diff --git a/src/components/score/score-animation.tsx b/src/components/score/score-animation.tsx new file mode 100644 index 0000000..ec648a6 --- /dev/null +++ b/src/components/score/score-animation.tsx @@ -0,0 +1,10 @@ +import { Variants } from "framer-motion"; + +/** + * The animation values for the score slide in animation. + */ +export const scoreAnimation: Variants = { + hiddenRight: { opacity: 0, x: 50 }, + hiddenLeft: { opacity: 0, x: -50 }, + visible: { opacity: 1, x: 0, transition: { staggerChildren: 0.03 } }, +};