better score animations
All checks were successful
Deploy SSR / deploy (push) Successful in 1m12s

This commit is contained in:
Lee 2024-09-12 23:26:02 +01:00
parent 39026d9a32
commit e60a14e799
3 changed files with 68 additions and 25 deletions

@ -1,4 +1,5 @@
import { PreloadResources } from "@/components/preload-resources"; import { PreloadResources } from "@/components/preload-resources";
import { AnimatePresence } from "framer-motion";
import type { Metadata } from "next"; import type { Metadata } from "next";
import localFont from "next/font/local"; import localFont from "next/font/local";
import BackgroundImage from "../components/background-image"; import BackgroundImage from "../components/background-image";
@ -67,10 +68,12 @@ export default function RootLayout({
<TooltipProvider> <TooltipProvider>
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange> <ThemeProvider attribute="class" defaultTheme="dark" enableSystem disableTransitionOnChange>
<QueryProvider> <QueryProvider>
<AnimatePresence>
<main className="z-[9999] m-auto flex h-screen flex-col items-center md:max-w-[1200px]"> <main className="z-[9999] m-auto flex h-screen flex-col items-center md:max-w-[1200px]">
<NavBar /> <NavBar />
{children} {children}
</main> </main>
</AnimatePresence>
</QueryProvider> </QueryProvider>
</ThemeProvider> </ThemeProvider>
</TooltipProvider> </TooltipProvider>

@ -47,7 +47,7 @@ export default function LeaderboardScores({ leaderboard }: Props) {
} }
return ( return (
<motion.div initial={{ opacity: 0, y: -50 }} animate={{ opacity: 1, y: 0 }}> <motion.div initial={{ opacity: 0, y: -50 }} exit={{ opacity: 0, y: -50 }} animate={{ opacity: 1, y: 0 }}>
<Card className="flex gap-2"> <Card className="flex gap-2">
<div className="text-center"> <div className="text-center">
{isError && <p>Oopsies! Something went wrong.</p>} {isError && <p>Oopsies! Something went wrong.</p>}

@ -8,7 +8,7 @@ import { capitalizeFirstLetter } from "@/common/string-utils";
import useWindowDimensions from "@/hooks/use-window-dimensions"; import useWindowDimensions from "@/hooks/use-window-dimensions";
import { ClockIcon, TrophyIcon } from "@heroicons/react/24/solid"; import { ClockIcon, TrophyIcon } from "@heroicons/react/24/solid";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { motion, useAnimation } from "framer-motion"; import { motion, useAnimation, Variants } from "framer-motion";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import Card from "../card"; import Card from "../card";
import Pagination from "../input/pagination"; import Pagination from "../input/pagination";
@ -35,11 +35,48 @@ type Props = {
page: number; page: number;
}; };
const containerVariants: Variants = {
hiddenRight: {
opacity: 0,
x: 50,
},
hiddenLeft: {
opacity: 0,
x: -50,
},
visible: {
opacity: 1,
x: 0,
transition: {
staggerChildren: 0.05,
},
},
};
const childVariants: Variants = {
hiddenRight: {
opacity: 0,
x: 50,
},
hiddenLeft: {
opacity: 0,
x: -50,
},
visible: {
opacity: 1,
x: 0,
transition: {
ease: "anticipate",
},
},
};
export default function PlayerScores({ initialScoreData, player, sort, page }: Props) { export default function PlayerScores({ initialScoreData, player, sort, page }: Props) {
const { width } = useWindowDimensions(); const { width } = useWindowDimensions();
const controls = useAnimation(); const controls = useAnimation();
const [currentSort, setCurrentSort] = useState(sort); const [currentSort, setCurrentSort] = useState(sort);
const [previousPage, setPreviousPage] = useState(page);
const [currentPage, setCurrentPage] = useState(page); const [currentPage, setCurrentPage] = useState(page);
const [currentScores, setCurrentScores] = useState<ScoreSaberPlayerScoresPage | undefined>(initialScoreData); const [currentScores, setCurrentScores] = useState<ScoreSaberPlayerScoresPage | undefined>(initialScoreData);
@ -54,23 +91,18 @@ export default function PlayerScores({ initialScoreData, player, sort, page }: P
staleTime: 30 * 1000, // Cache data for 30 seconds staleTime: 30 * 1000, // Cache data for 30 seconds
}); });
const handleAnimation = useCallback(() => { const handleScoreLoad = useCallback(async () => {
controls.set({ x: -50, opacity: 0 }); await controls.start(previousPage >= currentPage ? "hiddenRight" : "hiddenLeft");
controls.start({ x: 0, opacity: 1, transition: { duration: 0.25 } }); setCurrentScores(scores);
}, [controls]); await controls.start("visible");
}, [scores, controls]);
useEffect(() => { useEffect(() => {
if (scores) { if (scores) {
setCurrentScores(scores); handleScoreLoad();
} }
}, [scores]); }, [scores]);
useEffect(() => {
if (scores) {
handleAnimation();
}
}, [scores, handleAnimation]);
useEffect(() => { useEffect(() => {
const newUrl = `/player/${player.id}/${currentSort}/${currentPage}`; const newUrl = `/player/${player.id}/${currentSort}/${currentPage}`;
window.history.replaceState({ ...window.history.state, as: newUrl, url: newUrl }, "", newUrl); window.history.replaceState({ ...window.history.state, as: newUrl, url: newUrl }, "", newUrl);
@ -89,7 +121,7 @@ export default function PlayerScores({ initialScoreData, player, sort, page }: P
} }
return ( return (
<Card className="flex gap-2"> <Card className="flex gap-4">
<div className="flex items-center flex-row w-full gap-2 justify-center"> <div className="flex items-center flex-row w-full gap-2 justify-center">
{Object.values(scoreSort).map((sortOption, index) => ( {Object.values(scoreSort).map((sortOption, index) => (
<Button <Button
@ -110,12 +142,17 @@ export default function PlayerScores({ initialScoreData, player, sort, page }: P
{currentScores.playerScores.length === 0 && <p>No scores found. Invalid Page?</p>} {currentScores.playerScores.length === 0 && <p>No scores found. Invalid Page?</p>}
</div> </div>
<motion.div animate={controls}> <motion.div
<div className="grid min-w-full grid-cols-1 divide-y divide-border"> initial="hidden"
animate={controls}
variants={containerVariants}
className="grid min-w-full grid-cols-1 divide-y divide-border"
>
{currentScores.playerScores.map((playerScore, index) => ( {currentScores.playerScores.map((playerScore, index) => (
<Score key={index} playerScore={playerScore} /> <motion.div key={index} variants={childVariants}>
<Score playerScore={playerScore} />
</motion.div>
))} ))}
</div>
</motion.div> </motion.div>
<Pagination <Pagination
@ -123,7 +160,10 @@ export default function PlayerScores({ initialScoreData, player, sort, page }: P
page={currentPage} page={currentPage}
totalPages={Math.ceil(currentScores.metadata.total / currentScores.metadata.itemsPerPage)} totalPages={Math.ceil(currentScores.metadata.total / currentScores.metadata.itemsPerPage)}
loadingPage={isLoading ? currentPage : undefined} loadingPage={isLoading ? currentPage : undefined}
onPageChange={setCurrentPage} onPageChange={(page) => {
setPreviousPage(currentPage);
setCurrentPage(page);
}}
/> />
</Card> </Card>
); );