This commit is contained in:
@ -19,10 +19,16 @@ export default function ProfileButton() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={`/player/${settings.playerId}`} className="flex items-center gap-2 h-full">
|
||||
<Link
|
||||
href={`/player/${settings.playerId}`}
|
||||
className="flex items-center gap-2 h-full"
|
||||
>
|
||||
<NavbarButton>
|
||||
<Avatar className="w-6 h-6">
|
||||
<AvatarImage alt="Profile Picture" src={`https://cdn.scoresaber.com/avatars/${settings.playerId}.jpg`} />
|
||||
<AvatarImage
|
||||
alt="Profile Picture"
|
||||
src={`https://cdn.scoresaber.com/avatars/${settings.playerId}.jpg`}
|
||||
/>
|
||||
</Avatar>
|
||||
<p>You</p>
|
||||
</NavbarButton>
|
||||
|
32
src/components/player/player-badges.tsx
Normal file
32
src/components/player/player-badges.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
|
||||
import Image from "next/image";
|
||||
import Tooltip from "@/components/tooltip";
|
||||
|
||||
type Props = {
|
||||
player: ScoreSaberPlayer;
|
||||
};
|
||||
|
||||
export default function PlayerBadges({ player }: Props) {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 w-full items-center justify-center">
|
||||
{player.badges?.map((badge, index) => {
|
||||
return (
|
||||
<Tooltip
|
||||
side={"bottom"}
|
||||
key={index}
|
||||
display={<p>{badge.description}</p>}
|
||||
>
|
||||
<div>
|
||||
<Image
|
||||
src={badge.url}
|
||||
alt={badge.description}
|
||||
width={80}
|
||||
height={30}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import ScoreSaberPlayerScoresPageToken from "@/common/model/token/scoresaber/score-saber-player-scores-page-token";
|
||||
import ScoreSaberPlayerToken from "@/common/model/token/scoresaber/score-saber-player-token";
|
||||
import { scoresaberService } from "@/common/service/impl/scoresaber";
|
||||
import { ScoreSort } from "@/common/service/score-sort";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@ -9,17 +8,25 @@ import Mini from "../ranking/mini";
|
||||
import PlayerHeader from "./player-header";
|
||||
import PlayerRankChart from "./player-rank-chart";
|
||||
import PlayerScores from "./player-scores";
|
||||
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
|
||||
import Card from "@/components/card";
|
||||
import PlayerBadges from "@/components/player/player-badges";
|
||||
|
||||
const REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
type Props = {
|
||||
initialPlayerData: ScoreSaberPlayerToken;
|
||||
initialPlayerData: ScoreSaberPlayer;
|
||||
initialScoreData?: ScoreSaberPlayerScoresPageToken;
|
||||
sort: ScoreSort;
|
||||
page: number;
|
||||
};
|
||||
|
||||
export default function PlayerData({ initialPlayerData: initalPlayerData, initialScoreData, sort, page }: Props) {
|
||||
export default function PlayerData({
|
||||
initialPlayerData: initalPlayerData,
|
||||
initialScoreData,
|
||||
sort,
|
||||
page,
|
||||
}: Props) {
|
||||
let player = initalPlayerData;
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["player", player.id],
|
||||
@ -36,11 +43,17 @@ export default function PlayerData({ initialPlayerData: initalPlayerData, initia
|
||||
<article className="flex flex-col gap-2">
|
||||
<PlayerHeader player={player} />
|
||||
{!player.inactive && (
|
||||
<>
|
||||
<Card className="gap-1">
|
||||
<PlayerBadges player={player} />
|
||||
<PlayerRankChart player={player} />
|
||||
</>
|
||||
</Card>
|
||||
)}
|
||||
<PlayerScores initialScoreData={initialScoreData} player={player} sort={sort} page={page} />
|
||||
<PlayerScores
|
||||
initialScoreData={initialScoreData}
|
||||
player={player}
|
||||
sort={sort}
|
||||
page={page}
|
||||
/>
|
||||
</article>
|
||||
<aside className="w-[550px] hidden xl:flex flex-col gap-2">
|
||||
<Mini type="Global" player={player} />
|
||||
|
@ -1,4 +1,3 @@
|
||||
import ScoreSaberPlayerToken from "@/common/model/token/scoresaber/score-saber-player-token";
|
||||
import { formatNumberWithCommas, formatPp } from "@/common/number-utils";
|
||||
import { GlobeAmericasIcon } from "@heroicons/react/24/solid";
|
||||
import Card from "../card";
|
||||
@ -6,6 +5,7 @@ import CountryFlag from "../country-flag";
|
||||
import { Avatar, AvatarImage } from "../ui/avatar";
|
||||
import ClaimProfile from "./claim-profile";
|
||||
import PlayerStats from "./player-stats";
|
||||
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
|
||||
|
||||
const playerData = [
|
||||
{
|
||||
@ -13,29 +13,29 @@ const playerData = [
|
||||
icon: () => {
|
||||
return <GlobeAmericasIcon className="h-5 w-5" />;
|
||||
},
|
||||
render: (player: ScoreSaberPlayerToken) => {
|
||||
render: (player: ScoreSaberPlayer) => {
|
||||
return <p>#{formatNumberWithCommas(player.rank)}</p>;
|
||||
},
|
||||
},
|
||||
{
|
||||
showWhenInactiveOrBanned: false,
|
||||
icon: (player: ScoreSaberPlayerToken) => {
|
||||
icon: (player: ScoreSaberPlayer) => {
|
||||
return <CountryFlag code={player.country} size={15} />;
|
||||
},
|
||||
render: (player: ScoreSaberPlayerToken) => {
|
||||
render: (player: ScoreSaberPlayer) => {
|
||||
return <p>#{formatNumberWithCommas(player.countryRank)}</p>;
|
||||
},
|
||||
},
|
||||
{
|
||||
showWhenInactiveOrBanned: true,
|
||||
render: (player: ScoreSaberPlayerToken) => {
|
||||
render: (player: ScoreSaberPlayer) => {
|
||||
return <p className="text-pp">{formatPp(player.pp)}pp</p>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
type Props = {
|
||||
player: ScoreSaberPlayerToken;
|
||||
player: ScoreSaberPlayer;
|
||||
};
|
||||
|
||||
export default function PlayerHeader({ player }: Props) {
|
||||
@ -43,20 +43,27 @@ export default function PlayerHeader({ player }: Props) {
|
||||
<Card>
|
||||
<div className="flex gap-3 flex-col items-center text-center lg:flex-row lg:items-start lg:text-start relative select-none">
|
||||
<Avatar className="w-32 h-32 pointer-events-none">
|
||||
<AvatarImage alt="Profile Picture" src={player.profilePicture} />
|
||||
<AvatarImage alt="Profile Picture" src={player.avatar} />
|
||||
</Avatar>
|
||||
<div className="w-full flex gap-2 flex-col justify-center items-center lg:justify-start lg:items-start">
|
||||
<div>
|
||||
<p className="font-bold text-2xl">{player.name}</p>
|
||||
<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>}
|
||||
{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)) {
|
||||
if (
|
||||
!subName.showWhenInactiveOrBanned &&
|
||||
(player.inactive || player.banned)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,29 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
"use client";
|
||||
|
||||
import ScoreSaberPlayerToken from "@/common/model/token/scoresaber/score-saber-player-token";
|
||||
import { formatNumberWithCommas } from "@/common/number-utils";
|
||||
import { CategoryScale, Chart, Legend, LinearScale, LineElement, PointElement, Title, Tooltip } from "chart.js";
|
||||
import {
|
||||
CategoryScale,
|
||||
Chart,
|
||||
Legend,
|
||||
LinearScale,
|
||||
LineElement,
|
||||
PointElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
} from "chart.js";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import Card from "../card";
|
||||
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
|
||||
|
||||
Chart.register(LinearScale, CategoryScale, PointElement, LineElement, Title, Tooltip, Legend);
|
||||
Chart.register(
|
||||
LinearScale,
|
||||
CategoryScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
);
|
||||
|
||||
export const options: any = {
|
||||
maintainAspectRatio: false,
|
||||
@ -69,17 +85,12 @@ export const options: any = {
|
||||
};
|
||||
|
||||
type Props = {
|
||||
player: ScoreSaberPlayerToken;
|
||||
player: ScoreSaberPlayer;
|
||||
};
|
||||
|
||||
export default function PlayerRankChart({ player }: Props) {
|
||||
const playerRankHistory = player.histories.split(",").map((value) => {
|
||||
return parseInt(value);
|
||||
});
|
||||
playerRankHistory.push(player.rank);
|
||||
|
||||
const labels = [];
|
||||
for (let i = playerRankHistory.length; i > 0; i--) {
|
||||
for (let i = player.rankHistory.length; i > 0; i--) {
|
||||
let label = `${i} days ago`;
|
||||
if (i === 1) {
|
||||
label = "now";
|
||||
@ -95,7 +106,7 @@ export default function PlayerRankChart({ player }: Props) {
|
||||
datasets: [
|
||||
{
|
||||
lineTension: 0.5,
|
||||
data: playerRankHistory,
|
||||
data: player.rankHistory,
|
||||
label: "Rank",
|
||||
borderColor: "#606fff",
|
||||
fill: false,
|
||||
@ -105,8 +116,8 @@ export default function PlayerRankChart({ player }: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="h-96">
|
||||
<div className="h-96">
|
||||
<Line className="w-fit" options={options} data={data} />
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -11,13 +11,13 @@ import Pagination from "../input/pagination";
|
||||
import { Button } from "../ui/button";
|
||||
import { leaderboards } from "@/common/leaderboards";
|
||||
import { ScoreSort } from "@/common/service/score-sort";
|
||||
import ScoreSaberPlayerToken from "@/common/model/token/scoresaber/score-saber-player-token";
|
||||
import ScoreSaberPlayerScoresPageToken from "@/common/model/token/scoresaber/score-saber-player-scores-page-token";
|
||||
import Score from "@/components/score/score";
|
||||
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
|
||||
|
||||
type Props = {
|
||||
initialScoreData?: ScoreSaberPlayerScoresPageToken;
|
||||
player: ScoreSaberPlayerToken;
|
||||
player: ScoreSaberPlayer;
|
||||
sort: ScoreSort;
|
||||
page: number;
|
||||
};
|
||||
|
@ -1,59 +1,57 @@
|
||||
import { formatNumberWithCommas } from "@/common/number-utils";
|
||||
import StatValue from "@/components/stat-value";
|
||||
import ScoreSaberPlayerToken from "@/common/model/token/scoresaber/score-saber-player-token";
|
||||
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
|
||||
|
||||
type Badge = {
|
||||
name: string;
|
||||
color?: string;
|
||||
create: (
|
||||
player: ScoreSaberPlayerToken,
|
||||
) => string | React.ReactNode | undefined;
|
||||
create: (player: ScoreSaberPlayer) => string | React.ReactNode | undefined;
|
||||
};
|
||||
|
||||
const badges: Badge[] = [
|
||||
{
|
||||
name: "Ranked Play Count",
|
||||
color: "bg-pp",
|
||||
create: (player: ScoreSaberPlayerToken) => {
|
||||
return formatNumberWithCommas(player.scoreStats.rankedPlayCount);
|
||||
create: (player: ScoreSaberPlayer) => {
|
||||
return formatNumberWithCommas(player.statistics.rankedPlayCount);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Total Ranked Score",
|
||||
color: "bg-pp",
|
||||
create: (player: ScoreSaberPlayerToken) => {
|
||||
return formatNumberWithCommas(player.scoreStats.totalRankedScore);
|
||||
create: (player: ScoreSaberPlayer) => {
|
||||
return formatNumberWithCommas(player.statistics.totalRankedScore);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Average Ranked Accuracy",
|
||||
color: "bg-pp",
|
||||
create: (player: ScoreSaberPlayerToken) => {
|
||||
return player.scoreStats.averageRankedAccuracy.toFixed(2) + "%";
|
||||
create: (player: ScoreSaberPlayer) => {
|
||||
return player.statistics.averageRankedAccuracy.toFixed(2) + "%";
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Total Play Count",
|
||||
create: (player: ScoreSaberPlayerToken) => {
|
||||
return formatNumberWithCommas(player.scoreStats.totalPlayCount);
|
||||
create: (player: ScoreSaberPlayer) => {
|
||||
return formatNumberWithCommas(player.statistics.totalPlayCount);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Total Score",
|
||||
create: (player: ScoreSaberPlayerToken) => {
|
||||
return formatNumberWithCommas(player.scoreStats.totalScore);
|
||||
create: (player: ScoreSaberPlayer) => {
|
||||
return formatNumberWithCommas(player.statistics.totalScore);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Total Replays Watched",
|
||||
create: (player: ScoreSaberPlayerToken) => {
|
||||
return formatNumberWithCommas(player.scoreStats.replaysWatched);
|
||||
create: (player: ScoreSaberPlayer) => {
|
||||
return formatNumberWithCommas(player.statistics.replaysWatched);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
type Props = {
|
||||
player: ScoreSaberPlayerToken;
|
||||
player: ScoreSaberPlayer;
|
||||
};
|
||||
|
||||
export default function PlayerStats({ player }: Props) {
|
||||
|
@ -9,21 +9,25 @@ import { ReactElement } from "react";
|
||||
import Card from "../card";
|
||||
import CountryFlag from "../country-flag";
|
||||
import { Avatar, AvatarImage } from "../ui/avatar";
|
||||
import ScoreSaberPlayer from "@/common/model/player/impl/scoresaber-player";
|
||||
|
||||
const REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const PLAYER_NAME_MAX_LENGTH = 14;
|
||||
|
||||
type MiniProps = {
|
||||
type: "Global" | "Country";
|
||||
player: ScoreSaberPlayerToken;
|
||||
player: ScoreSaberPlayer;
|
||||
};
|
||||
|
||||
type Variants = {
|
||||
[key: string]: {
|
||||
itemsPerPage: number;
|
||||
icon: (player: ScoreSaberPlayerToken) => ReactElement;
|
||||
getPage: (player: ScoreSaberPlayerToken, itemsPerPage: number) => number;
|
||||
query: (page: number, country: string) => Promise<ScoreSaberPlayersPageToken | undefined>;
|
||||
icon: (player: ScoreSaberPlayer) => ReactElement;
|
||||
getPage: (player: ScoreSaberPlayer, itemsPerPage: number) => number;
|
||||
query: (
|
||||
page: number,
|
||||
country: string,
|
||||
) => Promise<ScoreSaberPlayersPageToken | undefined>;
|
||||
};
|
||||
};
|
||||
|
||||
@ -31,7 +35,7 @@ const miniVariants: Variants = {
|
||||
Global: {
|
||||
itemsPerPage: 50,
|
||||
icon: () => <GlobeAmericasIcon className="w-6 h-6" />,
|
||||
getPage: (player: ScoreSaberPlayerToken, itemsPerPage: number) => {
|
||||
getPage: (player: ScoreSaberPlayer, itemsPerPage: number) => {
|
||||
return Math.floor((player.rank - 1) / itemsPerPage) + 1;
|
||||
},
|
||||
query: (page: number) => {
|
||||
@ -40,14 +44,17 @@ const miniVariants: Variants = {
|
||||
},
|
||||
Country: {
|
||||
itemsPerPage: 50,
|
||||
icon: (player: ScoreSaberPlayerToken) => {
|
||||
icon: (player: ScoreSaberPlayer) => {
|
||||
return <CountryFlag code={player.country} size={12} />;
|
||||
},
|
||||
getPage: (player: ScoreSaberPlayerToken, itemsPerPage: number) => {
|
||||
getPage: (player: ScoreSaberPlayer, itemsPerPage: number) => {
|
||||
return Math.floor((player.countryRank - 1) / itemsPerPage) + 1;
|
||||
},
|
||||
query: (page: number, country: string) => {
|
||||
return leaderboards.ScoreSaber.queries.lookupGlobalPlayersByCountry(page, country);
|
||||
return leaderboards.ScoreSaber.queries.lookupGlobalPlayersByCountry(
|
||||
page,
|
||||
country,
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -107,7 +114,8 @@ export default function Mini({ type, player }: MiniProps) {
|
||||
{isLoading && <p className="text-gray-400">Loading...</p>}
|
||||
{isError && <p className="text-red-500">Error</p>}
|
||||
{players?.map((playerRanking, index) => {
|
||||
const rank = type == "Global" ? playerRanking.rank : playerRanking.countryRank;
|
||||
const rank =
|
||||
type == "Global" ? playerRanking.rank : playerRanking.countryRank;
|
||||
const playerName =
|
||||
playerRanking.name.length > PLAYER_NAME_MAX_LENGTH
|
||||
? playerRanking.name.substring(0, PLAYER_NAME_MAX_LENGTH) + "..."
|
||||
@ -122,9 +130,18 @@ export default function Mini({ type, player }: MiniProps) {
|
||||
<div className="flex gap-2">
|
||||
<p className="text-gray-400">#{formatNumberWithCommas(rank)}</p>
|
||||
<Avatar className="w-6 h-6 pointer-events-none">
|
||||
<AvatarImage alt="Profile Picture" src={playerRanking.profilePicture} />
|
||||
<AvatarImage
|
||||
alt="Profile Picture"
|
||||
src={playerRanking.profilePicture}
|
||||
/>
|
||||
</Avatar>
|
||||
<p className={playerRanking.id === player.id ? "text-gray-400" : ""}>{playerName}</p>
|
||||
<p
|
||||
className={
|
||||
playerRanking.id === player.id ? "text-gray-400" : ""
|
||||
}
|
||||
>
|
||||
{playerName}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-pp">{formatPp(playerRanking.pp)}pp</p>
|
||||
</Link>
|
||||
|
@ -14,9 +14,13 @@ type Props = {
|
||||
};
|
||||
|
||||
export default function ScoreSongInfo({ leaderboard, beatSaverMap }: Props) {
|
||||
const diff = getDifficultyFromScoreSaberDifficulty(leaderboard.difficulty.difficulty);
|
||||
const diff = getDifficultyFromScoreSaberDifficulty(
|
||||
leaderboard.difficulty.difficulty,
|
||||
);
|
||||
const mappersProfile =
|
||||
beatSaverMap != undefined ? `https://beatsaver.com/profile/${beatSaverMap?.fullData.uploader.id}` : undefined;
|
||||
beatSaverMap != undefined
|
||||
? `https://beatsaver.com/profile/${beatSaverMap?.fullData.uploader.id}`
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="flex gap-3 items-center">
|
||||
@ -68,7 +72,13 @@ export default function ScoreSongInfo({ leaderboard, beatSaverMap }: Props) {
|
||||
</p>
|
||||
<p className="text-sm text-gray-400">{leaderboard.songAuthorName}</p>
|
||||
<FallbackLink href={mappersProfile}>
|
||||
<p className={clsx("text-sm", mappersProfile && "hover:brightness-75 transform-gpu transition-all")}>
|
||||
<p
|
||||
className={clsx(
|
||||
"text-sm",
|
||||
mappersProfile &&
|
||||
"hover:brightness-75 transform-gpu transition-all",
|
||||
)}
|
||||
>
|
||||
{leaderboard.levelAuthorName}
|
||||
</p>
|
||||
</FallbackLink>
|
||||
|
@ -26,7 +26,9 @@ export default function ScoreRankInfo({ score }: Props) {
|
||||
</p>
|
||||
}
|
||||
>
|
||||
<p className="text-sm cursor-default">{timeAgo(new Date(score.timeSet))}</p>
|
||||
<p className="text-sm cursor-default">
|
||||
{timeAgo(new Date(score.timeSet))}
|
||||
</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
@ -8,10 +8,13 @@ import clsx from "clsx";
|
||||
|
||||
type Badge = {
|
||||
name: string;
|
||||
color?: (score: ScoreSaberScoreToken, leaderboard: ScoreSaberLeaderboardToken) => string | undefined;
|
||||
color?: (
|
||||
score: ScoreSaberScoreToken,
|
||||
leaderboard: ScoreSaberLeaderboardToken,
|
||||
) => string | undefined;
|
||||
create: (
|
||||
score: ScoreSaberScoreToken,
|
||||
leaderboard: ScoreSaberLeaderboardToken
|
||||
leaderboard: ScoreSaberLeaderboardToken,
|
||||
) => string | React.ReactNode | undefined;
|
||||
};
|
||||
|
||||
@ -31,11 +34,17 @@ const badges: Badge[] = [
|
||||
},
|
||||
{
|
||||
name: "Accuracy",
|
||||
color: (score: ScoreSaberScoreToken, leaderboard: ScoreSaberLeaderboardToken) => {
|
||||
color: (
|
||||
score: ScoreSaberScoreToken,
|
||||
leaderboard: ScoreSaberLeaderboardToken,
|
||||
) => {
|
||||
const acc = (score.baseScore / leaderboard.maxScore) * 100;
|
||||
return accuracyToColor(acc);
|
||||
},
|
||||
create: (score: ScoreSaberScoreToken, leaderboard: ScoreSaberLeaderboardToken) => {
|
||||
create: (
|
||||
score: ScoreSaberScoreToken,
|
||||
leaderboard: ScoreSaberLeaderboardToken,
|
||||
) => {
|
||||
const acc = (score.baseScore / leaderboard.maxScore) * 100;
|
||||
return `${acc.toFixed(2)}%`;
|
||||
},
|
||||
@ -61,8 +70,16 @@ const badges: Badge[] = [
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>{fullCombo ? <span className="text-green-400">FC</span> : formatNumberWithCommas(score.missedNotes)}</p>
|
||||
<XMarkIcon className={clsx("w-5 h-5", fullCombo ? "hidden" : "text-red-400")} />
|
||||
<p>
|
||||
{fullCombo ? (
|
||||
<span className="text-green-400">FC</span>
|
||||
) : (
|
||||
formatNumberWithCommas(score.missedNotes)
|
||||
)}
|
||||
</p>
|
||||
<XMarkIcon
|
||||
className={clsx("w-5 h-5", fullCombo ? "hidden" : "text-red-400")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
|
@ -1,4 +1,8 @@
|
||||
import { Tooltip as ShadCnTooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
||||
import {
|
||||
Tooltip as ShadCnTooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "./ui/tooltip";
|
||||
|
||||
type Props = {
|
||||
/**
|
||||
@ -10,13 +14,18 @@ type Props = {
|
||||
* What will be displayed in the tooltip
|
||||
*/
|
||||
display: React.ReactNode;
|
||||
|
||||
/**
|
||||
* Where the tooltip will be displayed
|
||||
*/
|
||||
side?: "top" | "bottom" | "left" | "right";
|
||||
};
|
||||
|
||||
export default function Tooltip({ children, display }: Props) {
|
||||
export default function Tooltip({ children, display, side = "top" }: Props) {
|
||||
return (
|
||||
<ShadCnTooltip>
|
||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||
<TooltipContent>{display}</TooltipContent>
|
||||
<TooltipContent side={side}>{display}</TooltipContent>
|
||||
</ShadCnTooltip>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user