add a simple page switch animation and show if the player is inactive or banned
All checks were successful
Deploy SSR / deploy (push) Successful in 1m38s
All checks were successful
Deploy SSR / deploy (push) Successful in 1m38s
This commit is contained in:
parent
763de454e7
commit
4d19fd3bfd
@ -25,6 +25,7 @@
|
||||
"clsx": "^2.1.1",
|
||||
"dexie": "^4.0.8",
|
||||
"dexie-react-hooks": "^1.1.7",
|
||||
"framer-motion": "^11.5.4",
|
||||
"ky": "^1.7.2",
|
||||
"lucide-react": "^0.439.0",
|
||||
"next": "14.2.9",
|
||||
|
22
pnpm-lock.yaml
generated
22
pnpm-lock.yaml
generated
@ -53,6 +53,9 @@ dependencies:
|
||||
dexie-react-hooks:
|
||||
specifier: ^1.1.7
|
||||
version: 1.1.7(@types/react@18.3.5)(dexie@4.0.8)(react@18.3.1)
|
||||
framer-motion:
|
||||
specifier: ^11.5.4
|
||||
version: 11.5.4(react-dom@18.3.1)(react@18.3.1)
|
||||
ky:
|
||||
specifier: ^1.7.2
|
||||
version: 1.7.2
|
||||
@ -2038,6 +2041,25 @@ packages:
|
||||
cross-spawn: 7.0.3
|
||||
signal-exit: 4.1.0
|
||||
|
||||
/framer-motion@11.5.4(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-E+tb3/G6SO69POkdJT+3EpdMuhmtCh9EWuK4I1DnIC23L7tFPrl8vxP+LSovwaw6uUr73rUbpb4FgK011wbRJQ==}
|
||||
peerDependencies:
|
||||
'@emotion/is-prop-valid': '*'
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
peerDependenciesMeta:
|
||||
'@emotion/is-prop-valid':
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
tslib: 2.7.0
|
||||
dev: false
|
||||
|
||||
/fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
dev: true
|
||||
|
@ -1,13 +0,0 @@
|
||||
type Props = {
|
||||
icon?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export default function PlayerDataPoint({ icon, children }: Props) {
|
||||
return (
|
||||
<div className="flex gap-1 items-center">
|
||||
{icon}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -31,7 +31,11 @@ export default function PlayerData({ initalPlayerData, sort, page }: Props) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<PlayerHeader player={player} />
|
||||
<PlayerRankChart player={player} />
|
||||
{!player.inactive && (
|
||||
<>
|
||||
<PlayerRankChart player={player} />
|
||||
</>
|
||||
)}
|
||||
<PlayerScores player={player} sort={sort} page={page} />
|
||||
</div>
|
||||
);
|
||||
|
@ -5,10 +5,10 @@ import Card from "../card";
|
||||
import CountryFlag from "../country-flag";
|
||||
import { Avatar, AvatarImage } from "../ui/avatar";
|
||||
import ClaimProfile from "./claim-profile";
|
||||
import PlayerDataPoint from "./player-data-point";
|
||||
|
||||
const playerSubNames = [
|
||||
const playerData = [
|
||||
{
|
||||
showWhenInactiveOrBanned: false,
|
||||
icon: () => {
|
||||
return <GlobeAmericasIcon className="h-5 w-5" />;
|
||||
},
|
||||
@ -17,6 +17,7 @@ const playerSubNames = [
|
||||
},
|
||||
},
|
||||
{
|
||||
showWhenInactiveOrBanned: false,
|
||||
icon: (player: ScoreSaberPlayer) => {
|
||||
return <CountryFlag country={player.country.toLowerCase()} size={15} />;
|
||||
},
|
||||
@ -25,6 +26,7 @@ const playerSubNames = [
|
||||
},
|
||||
},
|
||||
{
|
||||
showWhenInactiveOrBanned: true,
|
||||
render: (player: ScoreSaberPlayer) => {
|
||||
return <p className="text-pp">{formatNumberWithCommas(player.pp)}pp</p>;
|
||||
},
|
||||
@ -44,14 +46,26 @@ export default function PlayerHeader({ player }: Props) {
|
||||
</Avatar>
|
||||
<div>
|
||||
<p className="font-bold text-2xl">{player.name}</p>
|
||||
<div className="flex gap-2">
|
||||
{playerSubNames.map((subName, index) => {
|
||||
return (
|
||||
<PlayerDataPoint icon={subName.icon && subName.icon(player)} key={index}>
|
||||
{subName.render(player)}
|
||||
</PlayerDataPoint>
|
||||
);
|
||||
})}
|
||||
<div className="flex flex-col">
|
||||
<div>
|
||||
{player.inactive && <p className="text-gray-400">Inactive Account</p>}
|
||||
{player.banned && <p className="text-red-500">Banned Account</p>}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{playerData.map((subName, index) => {
|
||||
// Check if the player is inactive or banned and if the data should be shown
|
||||
if (!subName.showWhenInactiveOrBanned && (player.inactive || player.banned)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index} className="flex gap-1 items-center">
|
||||
{subName.icon && subName.icon(player)}
|
||||
{subName.render && subName.render(player)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute top-0 right-0">
|
||||
<ClaimProfile playerId={player.id} />
|
||||
|
@ -7,7 +7,8 @@ import ScoreSaberPlayerScoresPage from "@/common/data-fetcher/types/scoresaber/s
|
||||
import { capitalizeFirstLetter } from "@/common/string-utils";
|
||||
import useWindowDimensions from "@/hooks/use-window-dimensions";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useEffect, useState } from "react";
|
||||
import { motion, useAnimation } from "framer-motion";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import Card from "../card";
|
||||
import Pagination from "../input/pagination";
|
||||
import { Button } from "../ui/button";
|
||||
@ -21,6 +22,8 @@ type Props = {
|
||||
|
||||
export default function PlayerScores({ player, sort, page }: Props) {
|
||||
const { width } = useWindowDimensions();
|
||||
const controls = useAnimation();
|
||||
|
||||
const [currentSort, setCurrentSort] = useState(sort);
|
||||
const [currentPage, setCurrentPage] = useState(page);
|
||||
const [previousScores, setPreviousScores] = useState<ScoreSaberPlayerScoresPage | undefined>();
|
||||
@ -31,11 +34,25 @@ export default function PlayerScores({ player, sort, page }: Props) {
|
||||
staleTime: 30 * 1000, // Data will be cached for 30 seconds
|
||||
});
|
||||
|
||||
const handleAnimation = useCallback(() => {
|
||||
controls.set({
|
||||
x: -50,
|
||||
opacity: 0,
|
||||
});
|
||||
controls.start({
|
||||
x: 0,
|
||||
opacity: 1,
|
||||
transition: { duration: 0.25 },
|
||||
});
|
||||
}, [controls]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setPreviousScores(data);
|
||||
if (data == undefined) {
|
||||
return;
|
||||
}
|
||||
}, [data]);
|
||||
setPreviousScores(data);
|
||||
handleAnimation();
|
||||
}, [data, handleAnimation]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update URL and refetch data when currentSort or currentPage changes
|
||||
@ -80,11 +97,13 @@ export default function PlayerScores({ player, sort, page }: Props) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid min-w-full grid-cols-1 divide-y divide-border">
|
||||
{previousScores.playerScores.map((playerScore, index) => (
|
||||
<Score key={index} playerScore={playerScore} />
|
||||
))}
|
||||
</div>
|
||||
<motion.div animate={controls}>
|
||||
<div className="grid min-w-full grid-cols-1 divide-y divide-border">
|
||||
{previousScores.playerScores.map((playerScore, index) => (
|
||||
<Score key={index} playerScore={playerScore} />
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
<Pagination
|
||||
mobilePagination={width < 768}
|
||||
|
@ -84,6 +84,7 @@ export default function Score({ playerScore }: Props) {
|
||||
height={64}
|
||||
alt="Song Artwork"
|
||||
className="rounded-md min-w-[64px]"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className="flex">
|
||||
|
Reference in New Issue
Block a user