make tooltips look nicer
All checks were successful
deploy / deploy (push) Successful in 2m14s

This commit is contained in:
Lee
2023-10-29 15:35:10 +00:00
parent 20633c42eb
commit 204b02b36a
19 changed files with 915 additions and 179 deletions

View File

@ -1,3 +1,76 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 224 71.4% 4.1%;
--card: 0 0% 100%;
--card-foreground: 224 71.4% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 224 71.4% 4.1%;
--primary: 220.9 39.3% 11%;
--primary-foreground: 210 20% 98%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%;
--muted: 220 14.3% 95.9%;
--muted-foreground: 220 8.9% 46.1%;
--accent: 220 14.3% 95.9%;
--accent-foreground: 220.9 39.3% 11%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 20% 98%;
--border: 220 13% 91%;
--input: 220 13% 91%;
--ring: 224 71.4% 4.1%;
--radius: 0.5rem;
}
.dark {
--background: 224 71.4% 4.1%;
--foreground: 210 20% 98%;
--card: 224 71.4% 4.1%;
--card-foreground: 210 20% 98%;
--popover: 224 71.4% 4.1%;
--popover-foreground: 210 20% 98%;
--primary: 210 20% 98%;
--primary-foreground: 220.9 39.3% 11%;
--secondary: 215 27.9% 16.9%;
--secondary-foreground: 210 20% 98%;
--muted: 215 27.9% 16.9%;
--muted-foreground: 217.9 10.6% 64.9%;
--accent: 215 27.9% 16.9%;
--accent-foreground: 210 20% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%;
--ring: 216 12.2% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -3,6 +3,7 @@
import { useScoresaberScoresStore } from "@/store/scoresaberScoresStore";
import { useSettingsStore } from "@/store/settingsStore";
import React from "react";
import { TooltipProvider } from "./ui/Tooltip";
const UPDATE_INTERVAL = 1000 * 60 * 5; // 5 minutes
export default class AppProvider extends React.Component {
@ -47,6 +48,6 @@ export default class AppProvider extends React.Component {
render(): React.ReactNode {
const props: any = this.props;
return <>{props.children}</>;
return <TooltipProvider>{props.children}</TooltipProvider>;
}
}

View File

@ -1,19 +1,34 @@
import clsx from "clsx";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/Tooltip";
interface ButtonProps {
text: JSX.Element | string;
url: string;
text?: JSX.Element | string;
url?: string;
icon?: JSX.Element;
color?: string;
tooltip?: React.ReactNode;
className?: string;
onClick?: () => void;
}
export default function Button({ text, url, icon, className }: ButtonProps) {
return (
<a href={url}>
export default function Button({
text,
url,
icon,
color,
tooltip,
className,
onClick,
}: ButtonProps) {
if (!color) color = "bg-blue-500";
const base = (
<a href={url} onClick={onClick}>
<p
className={clsx(
"font-md flex w-fit transform-gpu flex-row items-center gap-1 rounded-md bg-blue-500 p-1 pl-2 pr-2 transition-all hover:opacity-80",
"font-md flex w-fit transform-gpu flex-row items-center gap-1 rounded-md p-1 transition-all hover:opacity-80",
className,
color,
)}
>
{icon}
@ -21,4 +36,14 @@ export default function Button({ text, url, icon, className }: ButtonProps) {
</p>
</a>
);
if (tooltip) {
return (
<Tooltip>
<TooltipTrigger>{base}</TooltipTrigger>
<TooltipContent>{tooltip}</TooltipContent>
</Tooltip>
);
}
return base;
}

View File

@ -1,24 +1,36 @@
import { normalizedRegionName } from "@/utils/utils";
import Image from "next/image";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/Tooltip";
type CountryFlagProps = {
className?: string;
countryCode: string;
tooltip?: React.ReactNode;
};
export default function CountyFlag({
className,
countryCode,
tooltip,
}: CountryFlagProps) {
return (
const base = (
<Image
src={`/assets/flags/${countryCode.toLowerCase()}.svg`}
alt={`${normalizedRegionName(countryCode)} flag`}
title={normalizedRegionName(countryCode)}
width={64}
height={64}
className={className}
priority
/>
);
if (tooltip) {
return (
<Tooltip>
<TooltipTrigger>{base}</TooltipTrigger>
<TooltipContent>{tooltip}</TooltipContent>
</Tooltip>
);
}
return base;
}

View File

@ -1,25 +1,25 @@
import clsx from "clsx";
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/Tooltip";
type LabelProps = {
title: string;
value: any;
hoverValue?: string;
tooltip?: React.ReactNode;
className?: string;
};
export default function Label({
title,
value,
hoverValue,
tooltip,
className = "bg-neutral-700",
}: LabelProps) {
return (
const base = (
<div
className={clsx(
"flex flex-col justify-center rounded-md hover:cursor-default",
className,
)}
title={hoverValue}
>
<div className="p4-[0.3rem] flex items-center gap-2 pb-[0.2rem] pl-[0.3rem] pr-[0.3rem] pt-[0.2rem]">
<p>{title}</p>
@ -28,4 +28,14 @@ export default function Label({
</div>
</div>
);
if (tooltip) {
return (
<Tooltip>
<TooltipTrigger>{base}</TooltipTrigger>
<TooltipContent>{tooltip}</TooltipContent>
</Tooltip>
);
}
return base;
}

View File

@ -77,7 +77,8 @@ export default function Navbar() {
return (
<Button
key={friend.id}
className="mt-2 bg-gray-500"
className="mt-2"
color="bg-gray-500"
text={friend.name}
url={`/player/${friend.id}/top/1`}
icon={

View File

@ -1,4 +1,5 @@
import Image from "next/image";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/Tooltip";
const headsets = [
{
@ -58,17 +59,26 @@ export default function HeadsetIcon({ id, size = 32, className }: IconProps) {
}
return (
<div className={className}>
<Image
src={`/assets/headsets/${headset.icon}`}
alt={headset.name}
title={headset.name}
width={size}
height={size}
style={{
filter: headset.filters,
}}
/>
</div>
<Tooltip>
<TooltipTrigger>
<div className={className}>
<Image
src={`/assets/headsets/${headset.icon}`}
alt={headset.name}
width={size}
height={size}
style={{
filter: headset.filters,
}}
/>
</div>
</TooltipTrigger>
<TooltipContent>
<div>
<p className="font-bold">Headset</p>
<p>{headset.name}</p>
</div>
</TooltipContent>
</Tooltip>
);
}

View File

@ -5,6 +5,7 @@ import { formatDate, formatTimeAgo } from "@/utils/timeUtils";
import Image from "next/image";
import Link from "next/link";
import ScoreStatLabel from "../player/ScoreStatLabel";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/Tooltip";
type ScoreProps = {
score: ScoresaberScore;
@ -25,12 +26,19 @@ export default function LeaderboardScore({
<div className="flex w-fit flex-row items-center justify-center gap-1">
<p>#{formatNumber(score.rank)}</p>
</div>
<p
className="hidden text-sm text-gray-200 md:block"
title={formatDate(score.timeSet)}
>
{formatTimeAgo(score.timeSet)}
</p>
<Tooltip>
<TooltipTrigger>
<p className="hidden text-sm text-gray-200 md:block">
{formatTimeAgo(score.timeSet)}
</p>
</TooltipTrigger>
<TooltipContent>
<div>
<p className="font-bold">Time Submitted</p>
<p>{formatDate(score.timeSet)}</p>
</div>
</TooltipContent>
</Tooltip>
</div>
{/* Song Image */}
<div className="flex w-full items-center gap-2 ">

View File

@ -18,6 +18,7 @@ import { useRef } from "react";
import { toast } from "react-toastify";
import { useStore } from "zustand";
import Avatar from "../Avatar";
import Button from "../Button";
import Card from "../Card";
import CountyFlag from "../CountryFlag";
import Label from "../Label";
@ -123,35 +124,29 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) {
{/* Settings Buttons */}
<div className="absolute right-3 top-20 flex flex-col justify-end gap-2 md:relative md:right-0 md:top-0 md:mt-2 md:flex-row md:justify-center">
{!isOwnProfile && (
<button
className="h-fit w-fit rounded-md bg-blue-500 p-1 hover:bg-blue-600"
<Button
onClick={claimProfile}
aria-label="Set as your Profile"
>
<HomeIcon title="Set as your Profile" width={24} height={24} />
</button>
tooltip={<p>Set as your Profile</p>}
icon={<HomeIcon width={24} height={24} />}
/>
)}
{!isOwnProfile && (
<>
{!settingsStore?.isFriend(playerId) && (
<button
className="rounded-md bg-blue-500 p-1 hover:opacity-80"
<Button
onClick={addFriend}
aria-label="Add as Friend"
>
<UserIcon title="Add as Friend" width={24} height={24} />
</button>
tooltip={<p>Add as Friend</p>}
icon={<UserIcon width={24} height={24} />}
/>
)}
{settingsStore.isFriend(playerId) && (
<button
className="rounded-md bg-red-500 p-1 hover:opacity-80"
<Button
onClick={removeFriend}
aria-label="Remove Friend"
>
<XMarkIcon title="Remove Friend" width={24} height={24} />
</button>
tooltip={<p>Remove Friend</p>}
icon={<XMarkIcon width={24} height={24} />}
/>
)}
</>
)}
@ -182,13 +177,16 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) {
href={`/ranking/country/${playerData.country}/${Math.round(
playerData.countryRank / 50,
)}`}
title={`${playerData.name} is from ${normalizedRegionName(
playerData.country,
)}`}
>
<CountyFlag
countryCode={playerData.country}
className="!h-7 !w-7"
tooltip={
<p>
{playerData.name} is from{" "}
{normalizedRegionName(playerData.country)}
</p>
}
/>
<p>#{formatNumber(playerData.countryRank)}</p>
</a>
@ -204,29 +202,43 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) {
<Label
title="Total play count"
className="bg-blue-500"
hoverValue={`Total ranked song play count | Total plays: ${formatNumber(
scoreStats.totalPlayCount,
)} | Ranked plays: ${formatNumber(scoreStats.rankedPlayCount)}`}
tooltip={
<div>
<p className="font-bold">Score counts</p>
<p>Total plays: {formatNumber(scoreStats.totalPlayCount)}</p>
<p>
Ranked plays: {formatNumber(scoreStats.rankedPlayCount)}
</p>
</div>
}
value={formatNumber(scoreStats.totalPlayCount)}
/>
<Label
title="Total score"
className="bg-blue-500"
hoverValue={`Total score of all your plays | Unranked score: ${formatNumber(
scoreStats.totalScore,
)} | Ranked score: ${formatNumber(scoreStats.totalRankedScore)} `}
tooltip={
<div>
<p className="font-bold">Raw score</p>
<p>Unranked score: {formatNumber(scoreStats.totalScore)}</p>
<p>
Ranked score: {formatNumber(scoreStats.totalRankedScore)}
</p>
</div>
}
value={formatNumber(scoreStats.totalScore)}
/>
<Label
title="Avg ranked acc"
className="bg-blue-500"
hoverValue="Average accuracy of all your ranked plays"
tooltip={<p>Average accuracy of all their ranked plays</p>}
value={`${scoreStats.averageRankedAccuracy.toFixed(2)}%`}
/>
<Label
title="Total replays watched"
className="bg-blue-500"
hoverValue="The total amount of times your replays have been watched"
tooltip={
<p>The total amount of times their replays have been watched</p>
}
value={formatNumber(scoreStats.replaysWatched)}
/>
@ -235,7 +247,7 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) {
<Label
title="Top PP"
className="bg-pp-blue"
hoverValue="Highest pp play"
tooltip={<p>Your highest pp play</p>}
value={`${formatNumber(
getHighestPpPlay(playerId)?.toFixed(2),
)}pp`}
@ -243,7 +255,9 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) {
<Label
title="Avg PP"
className="bg-pp-blue"
hoverValue="Average amount of pp per play (best 50 scores)"
tooltip={
<p>Average amount of pp per play (best 50 scores)</p>
}
value={`${formatNumber(
getAveragePp(playerId)?.toFixed(2),
)}pp`}
@ -251,7 +265,12 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) {
<Label
title="+ 1pp"
className="bg-pp-blue"
hoverValue="Amount of raw pp required to increase your global pp by 1pp"
tooltip={
<p>
Amount of raw pp required to increase your global pp by
1pp
</p>
}
value={`${formatNumber(
calcPpBoundary(playerId, 1)?.toFixed(2),
)}pp raw per global`}

View File

@ -11,6 +11,7 @@ import clsx from "clsx";
import dynamic from "next/dynamic";
import Image from "next/image";
import { useEffect, useState } from "react";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/Tooltip";
import PlayerChart from "./PlayerChart";
import PlayerInfo from "./PlayerInfo";
@ -99,14 +100,19 @@ export default function PlayerPage({ id, sort, page }: PlayerPageProps) {
>
{badges.map((badge) => {
return (
<Image
key={badge.image}
src={badge.image}
alt={badge.description}
title={badge.description}
width={80}
height={30}
/>
<Tooltip key={badge.image}>
<TooltipTrigger>
<Image
src={badge.image}
alt={badge.description}
width={80}
height={30}
/>
</TooltipTrigger>
<TooltipContent>
<p>{badge.description}</p>
</TooltipContent>
</Tooltip>
);
})}
</div>

View File

@ -39,12 +39,12 @@ export default function PlayerRankingMobile({
<div className="flex flex-wrap justify-center gap-2">
<Label
title="PP"
hoverValue="Total amount of pp"
tooltip={<p>The total amount of pp this player has</p>}
value={`${formatNumber(player.pp)}`}
/>
<Label
title="Total play count"
hoverValue="Total ranked song play count"
tooltip={<p>The total amount of plays this player has</p>}
value={formatNumber(player.scoreStats.totalPlayCount)}
/>
</div>

View File

@ -18,6 +18,7 @@ import clsx from "clsx";
import Image from "next/image";
import Link from "next/link";
import HeadsetIcon from "../icons/HeadsetIcon";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/Tooltip";
import ScoreStatLabel from "./ScoreStatLabel";
type ScoreProps = {
@ -34,6 +35,8 @@ export default function Score({ score, player, leaderboard }: ScoreProps) {
const diffColor = songDifficultyToColor(diffName);
const accuracy = ((score.baseScore / leaderboard.maxScore) * 100).toFixed(2);
const totalMissedNotes = score.missedNotes + score.badCuts;
const weightedPp =
formatNumber(getPpGainedFromScore(player.id, score), 2) + "pp";
return (
<div className="grid grid-cols-1 pb-2 pt-2 first:pt-0 last:pb-0 md:grid-cols-[0.85fr_6fr_1.3fr]">
@ -43,12 +46,19 @@ export default function Score({ score, player, leaderboard }: ScoreProps) {
<p>#{formatNumber(score.rank)}</p>
<HeadsetIcon id={score.hmd} size={20} />
</div>
<p
className="hidden text-sm text-gray-200 md:block"
title={formatDate(score.timeSet)}
>
{formatTimeAgo(score.timeSet)}
</p>
<Tooltip>
<TooltipTrigger>
<p className="text-sm text-gray-200">
{formatTimeAgo(score.timeSet)}
</p>
</TooltipTrigger>
<TooltipContent>
<div>
<p className="font-bold">Time Submitted</p>
<p>{formatDate(score.timeSet)}</p>
</div>
</TooltipContent>
</Tooltip>
</div>
{/* Song Image */}
<div className="flex w-full items-center gap-2">
@ -105,12 +115,19 @@ export default function Score({ score, player, leaderboard }: ScoreProps) {
{/* Time Set (Mobile) */}
<div>
<p
className="block text-sm text-gray-200 md:hidden"
title={formatDate(score.timeSet)}
>
{formatTimeAgo(score.timeSet)}
</p>
<Tooltip>
<TooltipTrigger>
<p className="text-sm text-gray-200">
{formatTimeAgo(score.timeSet)}
</p>
</TooltipTrigger>
<TooltipContent>
<div>
<p className="font-bold">Time Submitted</p>
<p>{formatDate(score.timeSet)}</p>
</div>
</TooltipContent>
</Tooltip>
</div>
</div>
@ -121,15 +138,24 @@ export default function Score({ score, player, leaderboard }: ScoreProps) {
<ScoreStatLabel
className="bg-blue-500 text-center"
value={formatNumber(score.pp.toFixed(2)) + "pp"}
title={`Weighted pp ${formatNumber(
getPpGainedFromScore(player.id, score),
2,
)}pp`}
tooltip={
<div>
<p className="font-bold">Performance Points</p>
<p>Weighted PP: {weightedPp}</p>
</div>
}
/>
)}
{/* Percentage score */}
<ScoreStatLabel
tooltip={
<div>
<p className="font-bold">Score</p>
<p>Accuracy: {accuracy}%</p>
<p>Raw Score: {formatNumber(score.baseScore)}</p>
</div>
}
value={
!leaderboard.maxScore
? formatNumber(score.baseScore)
@ -145,8 +171,12 @@ export default function Score({ score, player, leaderboard }: ScoreProps) {
"min-w-[2rem]",
isFullCombo ? "bg-green-500" : "bg-red-500",
)}
title={
isFullCombo ? "Full Combo" : `${totalMissedNotes}x Missed Notes`
tooltip={
<div>
<p className="font-bold">Mistakes</p>
<p>Misses: {score.missedNotes}</p>
<p>Bad Cuts: {score.badCuts}</p>
</div>
}
icon={
isFullCombo ? (

View File

@ -1,19 +1,20 @@
import clsx from "clsx";
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/Tooltip";
type LabelProps = {
value: string;
title?: string;
tooltip?: React.ReactNode;
icon?: JSX.Element;
className?: string;
};
export default function ScoreStatLabel({
value,
title,
tooltip,
icon,
className = "bg-neutral-700",
}: LabelProps) {
return (
const base = (
<div
className={clsx(
"flex flex-col rounded-md hover:cursor-default",
@ -21,14 +22,21 @@ export default function ScoreStatLabel({
)}
>
<div className="p4-[0.3rem] flex items-center gap-2 pb-[0.2rem] pl-[0.3rem] pr-[0.3rem] pt-[0.2rem]">
<p
className="flex w-full items-center justify-center gap-1"
title={title}
>
<p className="flex w-full items-center justify-center gap-1">
{value}
{icon}
</p>
</div>
</div>
);
if (tooltip) {
return (
<Tooltip>
<TooltipTrigger>{base}</TooltipTrigger>
<TooltipContent>{tooltip}</TooltipContent>
</Tooltip>
);
}
return base;
}

View File

@ -0,0 +1,30 @@
"use client";
import { cn } from "@/utils/utils";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import * as React from "react";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 overflow-hidden rounded-md border px-3 py-1.5 text-sm shadow-md",
"border-none bg-zinc-800 text-white",
className,
)}
{...props}
/>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger };

View File

@ -1,7 +1,13 @@
import { clsx, type ClassValue } from "clsx";
import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
import { twMerge } from "tailwind-merge";
let regionNames = new Intl.DisplayNames(["en"], { type: "region" });
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
export function isProduction() {
return process.env.NODE_ENV === "production";
}