feat(ssr): server side render parts of the navbar
All checks were successful
deploy / deploy (push) Successful in 1m14s
All checks were successful
deploy / deploy (push) Successful in 1m14s
This commit is contained in:
parent
9637993471
commit
f2801f2ae7
@ -1,6 +1,6 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
import Navbar from "./Navbar";
|
import Navbar from "./navbar/Navbar";
|
||||||
|
|
||||||
export default function Container({ children }: { children: React.ReactNode }) {
|
export default function Container({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
|
@ -1,162 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useSettingsStore } from "@/store/settingsStore";
|
|
||||||
import useStore from "@/utils/useStore";
|
|
||||||
import {
|
|
||||||
CogIcon,
|
|
||||||
MagnifyingGlassIcon,
|
|
||||||
ServerIcon,
|
|
||||||
UserIcon,
|
|
||||||
} from "@heroicons/react/20/solid";
|
|
||||||
import { GlobeAltIcon, TvIcon } from "@heroicons/react/24/outline";
|
|
||||||
import Link from "next/link";
|
|
||||||
import Avatar from "./Avatar";
|
|
||||||
import { Button } from "./ui/button";
|
|
||||||
import { Card } from "./ui/card";
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
|
|
||||||
|
|
||||||
interface ButtonProps {
|
|
||||||
text: string;
|
|
||||||
icon?: JSX.Element;
|
|
||||||
href?: string;
|
|
||||||
ariaLabel: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function NavbarButton({ text, icon, href, ariaLabel }: ButtonProps) {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
aria-label={ariaLabel}
|
|
||||||
className="flex h-full w-fit transform-gpu items-center justify-center gap-1 rounded-md p-[8px] transition-all hover:cursor-pointer hover:bg-blue-500"
|
|
||||||
href={href}
|
|
||||||
>
|
|
||||||
<>
|
|
||||||
{icon}
|
|
||||||
<p className="hidden md:block">{text}</p>
|
|
||||||
</>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function FriendsButton() {
|
|
||||||
const settingsStore = useStore(useSettingsStore, (state) => state);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<NavbarButton
|
|
||||||
ariaLabel="View your friends"
|
|
||||||
text="Friends"
|
|
||||||
icon={<UserIcon height={23} width={23} />}
|
|
||||||
/>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="p-2">
|
|
||||||
{settingsStore?.friends.length == 0 ? (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div>
|
|
||||||
<p className="text-md font-bold">No friends</p>
|
|
||||||
<p className="text-sm text-gray-400">
|
|
||||||
Add new friends by clicking below
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Link href={"/search"}>
|
|
||||||
<Button className="w-full" size={"sm"}>
|
|
||||||
Search
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
settingsStore?.friends.map((friend) => {
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={friend.id}
|
|
||||||
href={`/player/${friend.id}/top/1`}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
<div className="flex transform-gpu gap-2 rounded-md p-2 text-left transition-all hover:bg-background">
|
|
||||||
<Avatar
|
|
||||||
url={friend.profilePicture}
|
|
||||||
label="Friend avatar"
|
|
||||||
size={48}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-400">#{friend.rank}</p>
|
|
||||||
<p>{friend.name}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Navbar() {
|
|
||||||
const settingsStore = useStore(useSettingsStore, (state) => state);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Card className="flex h-fit w-full rounded-md">
|
|
||||||
{settingsStore !== undefined && settingsStore.player && (
|
|
||||||
<NavbarButton
|
|
||||||
ariaLabel="Your profile"
|
|
||||||
text="You"
|
|
||||||
icon={
|
|
||||||
<Avatar
|
|
||||||
url={settingsStore.player.profilePicture}
|
|
||||||
label="Your avatar"
|
|
||||||
size={23}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
href={`/player/${settingsStore.player.id}/top/1`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FriendsButton />
|
|
||||||
{/* TODO: fix hydration error? */}
|
|
||||||
{/* <Tooltip>
|
|
||||||
<TooltipTrigger>
|
|
||||||
<FriendsButton />
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>Click to view your friends</TooltipContent>
|
|
||||||
</Tooltip> */}
|
|
||||||
|
|
||||||
<NavbarButton
|
|
||||||
ariaLabel="View the global ranking"
|
|
||||||
text="Ranking"
|
|
||||||
icon={<GlobeAltIcon height={23} width={23} />}
|
|
||||||
href="/ranking/global/1"
|
|
||||||
/>
|
|
||||||
<NavbarButton
|
|
||||||
ariaLabel="View the overlay builder"
|
|
||||||
text="Overlay"
|
|
||||||
icon={<TvIcon height={23} width={23} />}
|
|
||||||
href="/overlay/builder"
|
|
||||||
/>
|
|
||||||
<NavbarButton
|
|
||||||
ariaLabel="View analytics for Scoresaber"
|
|
||||||
text="Analytics"
|
|
||||||
icon={<ServerIcon height={23} width={23} />}
|
|
||||||
href="/analytics"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="m-auto" />
|
|
||||||
|
|
||||||
<NavbarButton
|
|
||||||
ariaLabel="Search for a player"
|
|
||||||
text="Search"
|
|
||||||
icon={<MagnifyingGlassIcon height={23} width={23} />}
|
|
||||||
href="/search"
|
|
||||||
/>
|
|
||||||
<NavbarButton
|
|
||||||
ariaLabel="View your settings"
|
|
||||||
text="Settings"
|
|
||||||
icon={<CogIcon height={23} width={23} />}
|
|
||||||
href="/settings"
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
66
src/components/navbar/FriendsButton.tsx
Normal file
66
src/components/navbar/FriendsButton.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useSettingsStore } from "@/store/settingsStore";
|
||||||
|
import useStore from "@/utils/useStore";
|
||||||
|
import { UserIcon } from "@heroicons/react/20/solid";
|
||||||
|
import Link from "next/link";
|
||||||
|
import Avatar from "../Avatar";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
||||||
|
import NavbarButton from "./NavbarButton";
|
||||||
|
|
||||||
|
export default function FriendsButton() {
|
||||||
|
const settingsStore = useStore(useSettingsStore, (state) => state);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<NavbarButton
|
||||||
|
ariaLabel="View your friends"
|
||||||
|
text="Friends"
|
||||||
|
icon={<UserIcon height={23} width={23} />}
|
||||||
|
/>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-2">
|
||||||
|
{settingsStore?.friends.length == 0 ? (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div>
|
||||||
|
<p className="text-md font-bold">No friends</p>
|
||||||
|
<p className="text-sm text-gray-400">
|
||||||
|
Add new friends by clicking below
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link href={"/search"}>
|
||||||
|
<Button className="w-full" size={"sm"}>
|
||||||
|
Search
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
settingsStore?.friends.map((friend) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={friend.id}
|
||||||
|
href={`/player/${friend.id}/top/1`}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<div className="flex transform-gpu gap-2 rounded-md p-2 text-left transition-all hover:bg-background">
|
||||||
|
<Avatar
|
||||||
|
url={friend.profilePicture}
|
||||||
|
label="Friend avatar"
|
||||||
|
size={48}
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-400">#{friend.rank}</p>
|
||||||
|
<p>{friend.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
}
|
56
src/components/navbar/Navbar.tsx
Normal file
56
src/components/navbar/Navbar.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
CogIcon,
|
||||||
|
MagnifyingGlassIcon,
|
||||||
|
ServerIcon,
|
||||||
|
} from "@heroicons/react/20/solid";
|
||||||
|
import { GlobeAltIcon, TvIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { Card } from "../ui/card";
|
||||||
|
import FriendsButton from "./FriendsButton";
|
||||||
|
import NavbarButton from "./NavbarButton";
|
||||||
|
import YouButton from "./YouButton";
|
||||||
|
|
||||||
|
export default function Navbar() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Card className="flex h-fit w-full rounded-md">
|
||||||
|
<YouButton />
|
||||||
|
|
||||||
|
<FriendsButton />
|
||||||
|
|
||||||
|
<NavbarButton
|
||||||
|
ariaLabel="View the global ranking"
|
||||||
|
text="Ranking"
|
||||||
|
icon={<GlobeAltIcon height={23} width={23} />}
|
||||||
|
href="/ranking/global/1"
|
||||||
|
/>
|
||||||
|
<NavbarButton
|
||||||
|
ariaLabel="View the overlay builder"
|
||||||
|
text="Overlay"
|
||||||
|
icon={<TvIcon height={23} width={23} />}
|
||||||
|
href="/overlay/builder"
|
||||||
|
/>
|
||||||
|
<NavbarButton
|
||||||
|
ariaLabel="View analytics for Scoresaber"
|
||||||
|
text="Analytics"
|
||||||
|
icon={<ServerIcon height={23} width={23} />}
|
||||||
|
href="/analytics"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="m-auto" />
|
||||||
|
|
||||||
|
<NavbarButton
|
||||||
|
ariaLabel="Search for a player"
|
||||||
|
text="Search"
|
||||||
|
icon={<MagnifyingGlassIcon height={23} width={23} />}
|
||||||
|
href="/search"
|
||||||
|
/>
|
||||||
|
<NavbarButton
|
||||||
|
ariaLabel="View your settings"
|
||||||
|
text="Settings"
|
||||||
|
icon={<CogIcon height={23} width={23} />}
|
||||||
|
href="/settings"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
26
src/components/navbar/NavbarButton.tsx
Normal file
26
src/components/navbar/NavbarButton.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
interface ButtonProps {
|
||||||
|
text: string;
|
||||||
|
icon?: JSX.Element;
|
||||||
|
href?: string;
|
||||||
|
ariaLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NavbarButton({
|
||||||
|
text,
|
||||||
|
icon,
|
||||||
|
href,
|
||||||
|
ariaLabel,
|
||||||
|
}: ButtonProps) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
className="flex h-full w-fit transform-gpu items-center justify-center gap-1 rounded-md p-[8px] transition-all hover:cursor-pointer hover:bg-blue-500"
|
||||||
|
href={href}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
{icon}
|
||||||
|
<p className="hidden md:block">{text}</p>
|
||||||
|
</>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
29
src/components/navbar/YouButton.tsx
Normal file
29
src/components/navbar/YouButton.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useSettingsStore } from "@/store/settingsStore";
|
||||||
|
import { useStore } from "zustand";
|
||||||
|
import Avatar from "../Avatar";
|
||||||
|
import NavbarButton from "./NavbarButton";
|
||||||
|
|
||||||
|
export default function YouButton() {
|
||||||
|
const settingsStore = useStore(useSettingsStore, (state) => state);
|
||||||
|
|
||||||
|
if (!settingsStore || !settingsStore.player) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavbarButton
|
||||||
|
ariaLabel="Your profile"
|
||||||
|
text="You"
|
||||||
|
icon={
|
||||||
|
<Avatar
|
||||||
|
url={settingsStore.player.profilePicture}
|
||||||
|
label="Your avatar"
|
||||||
|
size={23}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
href={`/player/${settingsStore.player.id}/top/1`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,120 +1,19 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||||
import { useScoresaberScoresStore } from "@/store/scoresaberScoresStore";
|
|
||||||
import { useSettingsStore } from "@/store/settingsStore";
|
|
||||||
import { formatNumber } from "@/utils/numberUtils";
|
import { formatNumber } from "@/utils/numberUtils";
|
||||||
import { getAveragePp, getHighestPpPlay } from "@/utils/scoresaber/scores";
|
|
||||||
import { normalizedRegionName } from "@/utils/utils";
|
import { normalizedRegionName } from "@/utils/utils";
|
||||||
import {
|
import { GlobeAsiaAustraliaIcon } from "@heroicons/react/20/solid";
|
||||||
GlobeAsiaAustraliaIcon,
|
|
||||||
HomeIcon,
|
|
||||||
UserIcon,
|
|
||||||
XMarkIcon,
|
|
||||||
} from "@heroicons/react/20/solid";
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { toast } from "react-toastify";
|
|
||||||
import { useStore } from "zustand";
|
|
||||||
import Avatar from "../Avatar";
|
import Avatar from "../Avatar";
|
||||||
import Button from "../Button";
|
|
||||||
import Card from "../Card";
|
import Card from "../Card";
|
||||||
import CountyFlag from "../CountryFlag";
|
import CountyFlag from "../CountryFlag";
|
||||||
import Label from "../Label";
|
import Label from "../Label";
|
||||||
|
import PlayerInfoExtraLabels from "./PlayerInfoExtraLabels";
|
||||||
const PPGainLabel = dynamic(() => import("./PPGainLabel"));
|
import PlayerSettingsButtons from "./PlayerSettingsButtons";
|
||||||
|
|
||||||
type PlayerInfoProps = {
|
type PlayerInfoProps = {
|
||||||
playerData: ScoresaberPlayer;
|
playerData: ScoresaberPlayer;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function PlayerInfo({ playerData }: PlayerInfoProps) {
|
export default function PlayerInfo({ playerData }: PlayerInfoProps) {
|
||||||
const [mounted, setMounted] = useState(false);
|
|
||||||
const playerId = playerData.id;
|
|
||||||
const settingsStore = useStore(useSettingsStore, (store) => store);
|
|
||||||
const playerScoreStore = useStore(useScoresaberScoresStore, (store) => store);
|
|
||||||
|
|
||||||
// Whether we have scores for this player in the local database
|
|
||||||
const hasLocalScores = playerScoreStore?.exists(playerId);
|
|
||||||
|
|
||||||
const toastId: any = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMounted(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function claimProfile() {
|
|
||||||
settingsStore?.setProfile(playerData);
|
|
||||||
addProfile(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addFriend() {
|
|
||||||
const friend = await settingsStore?.addFriend(playerData.id);
|
|
||||||
if (!friend) {
|
|
||||||
toast.error(`Failed to add ${playerData.name} as a friend`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
addProfile(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeFriend() {
|
|
||||||
settingsStore?.removeFriend(playerData.id);
|
|
||||||
|
|
||||||
toast.success(`Successfully removed ${playerData.name} as a friend`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addProfile(isFriend: boolean) {
|
|
||||||
if (!useScoresaberScoresStore.getState().exists(playerId)) {
|
|
||||||
if (!isFriend) {
|
|
||||||
toast.success(`Successfully set ${playerData.name} as your profile`);
|
|
||||||
} else {
|
|
||||||
toast.success(`Successfully added ${playerData.name} as a friend`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const reponse = await playerScoreStore?.addOrUpdatePlayer(
|
|
||||||
playerId,
|
|
||||||
(page, totalPages) => {
|
|
||||||
const autoClose = page == totalPages ? 5000 : false;
|
|
||||||
|
|
||||||
if (page == 1) {
|
|
||||||
toastId.current = toast.info(
|
|
||||||
`Fetching scores for ${playerData.name} page ${page}/${totalPages}`,
|
|
||||||
{
|
|
||||||
autoClose: autoClose,
|
|
||||||
progress: page / totalPages,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (page != totalPages) {
|
|
||||||
toast.update(toastId.current, {
|
|
||||||
progress: page / totalPages,
|
|
||||||
render: `Fetching scores for ${playerData.name} page ${page}/${totalPages}`,
|
|
||||||
autoClose: autoClose,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toast.update(toastId.current, {
|
|
||||||
progress: 0,
|
|
||||||
render: `Successfully fetched scores for ${playerData.name}`,
|
|
||||||
autoClose: autoClose,
|
|
||||||
type: "success",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Fetching scores for ${playerId} (${page}/${totalPages})`,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (reponse?.error) {
|
|
||||||
toast.error("Failed to fetch scores");
|
|
||||||
console.log(reponse.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isOwnProfile = settingsStore.player?.id == playerId;
|
|
||||||
const scoreStats = playerData.scoreStats;
|
const scoreStats = playerData.scoreStats;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -129,42 +28,7 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) {
|
|||||||
|
|
||||||
{/* Settings Buttons */}
|
{/* 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">
|
<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">
|
||||||
{mounted && (
|
<PlayerSettingsButtons playerData={playerData} />
|
||||||
<>
|
|
||||||
{!isOwnProfile && (
|
|
||||||
<Button
|
|
||||||
onClick={claimProfile}
|
|
||||||
tooltip={<p>Set as your Profile</p>}
|
|
||||||
icon={<HomeIcon width={24} height={24} />}
|
|
||||||
ariaLabel="Set as your Profile"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isOwnProfile && (
|
|
||||||
<>
|
|
||||||
{!settingsStore?.isFriend(playerId) && (
|
|
||||||
<Button
|
|
||||||
onClick={addFriend}
|
|
||||||
tooltip={<p>Add as Friend</p>}
|
|
||||||
icon={<UserIcon width={24} height={24} />}
|
|
||||||
color="bg-green-500"
|
|
||||||
ariaLabel="Add Friend"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{settingsStore.isFriend(playerId) && (
|
|
||||||
<Button
|
|
||||||
onClick={removeFriend}
|
|
||||||
tooltip={<p>Remove Friend</p>}
|
|
||||||
icon={<XMarkIcon width={24} height={24} />}
|
|
||||||
color="bg-red-500"
|
|
||||||
ariaLabel="Remove Friend"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -261,30 +125,7 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) {
|
|||||||
value={formatNumber(scoreStats.replaysWatched)}
|
value={formatNumber(scoreStats.replaysWatched)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{hasLocalScores && (
|
<PlayerInfoExtraLabels playerId={playerData.id} />
|
||||||
<>
|
|
||||||
<Label
|
|
||||||
title="Top PP"
|
|
||||||
className="bg-pp-blue"
|
|
||||||
tooltip={<p>Their highest pp play</p>}
|
|
||||||
value={`${formatNumber(
|
|
||||||
getHighestPpPlay(playerId)?.toFixed(2),
|
|
||||||
)}pp`}
|
|
||||||
/>
|
|
||||||
<Label
|
|
||||||
title="Avg PP"
|
|
||||||
className="bg-pp-blue"
|
|
||||||
tooltip={
|
|
||||||
<p>Average amount of pp per play (best 50 scores)</p>
|
|
||||||
}
|
|
||||||
value={`${formatNumber(
|
|
||||||
getAveragePp(playerId)?.toFixed(2),
|
|
||||||
)}pp`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PPGainLabel playerId={playerId} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
42
src/components/player/PlayerInfoExtraLabels.tsx
Normal file
42
src/components/player/PlayerInfoExtraLabels.tsx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useScoresaberScoresStore } from "@/store/scoresaberScoresStore";
|
||||||
|
import { formatNumber } from "@/utils/numberUtils";
|
||||||
|
import { getAveragePp, getHighestPpPlay } from "@/utils/scoresaber/scores";
|
||||||
|
import { useStore } from "zustand";
|
||||||
|
import Label from "../Label";
|
||||||
|
import PPGainLabel from "./PPGainLabel";
|
||||||
|
|
||||||
|
type PlayerInfoExtraLabelsProps = {
|
||||||
|
playerId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PlayerInfoExtraLabels({
|
||||||
|
playerId,
|
||||||
|
}: PlayerInfoExtraLabelsProps) {
|
||||||
|
const playerScoreStore = useStore(useScoresaberScoresStore, (store) => store);
|
||||||
|
const hasLocalScores = playerScoreStore.exists(playerId);
|
||||||
|
|
||||||
|
if (!hasLocalScores) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Label
|
||||||
|
title="Top PP"
|
||||||
|
className="bg-pp-blue"
|
||||||
|
tooltip={<p>Their highest pp play</p>}
|
||||||
|
value={`${formatNumber(getHighestPpPlay(playerId)?.toFixed(2))}pp`}
|
||||||
|
/>
|
||||||
|
<Label
|
||||||
|
title="Avg PP"
|
||||||
|
className="bg-pp-blue"
|
||||||
|
tooltip={<p>Average amount of pp per play (best 50 scores)</p>}
|
||||||
|
value={`${formatNumber(getAveragePp(playerId)?.toFixed(2))}pp`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PPGainLabel playerId={playerId} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
137
src/components/player/PlayerSettingsButtons.tsx
Normal file
137
src/components/player/PlayerSettingsButtons.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||||
|
import { useScoresaberScoresStore } from "@/store/scoresaberScoresStore";
|
||||||
|
import { useSettingsStore } from "@/store/settingsStore";
|
||||||
|
import { HomeIcon, UserIcon, XMarkIcon } from "@heroicons/react/20/solid";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { useStore } from "zustand";
|
||||||
|
import Button from "../Button";
|
||||||
|
|
||||||
|
type PlayerSettingsButtonsProps = {
|
||||||
|
playerData: ScoresaberPlayer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PlayerSettingsButtons({
|
||||||
|
playerData,
|
||||||
|
}: PlayerSettingsButtonsProps) {
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
const playerId = playerData.id;
|
||||||
|
|
||||||
|
const settingsStore = useStore(useSettingsStore, (store) => store);
|
||||||
|
const playerScoreStore = useStore(useScoresaberScoresStore, (store) => store);
|
||||||
|
|
||||||
|
const isOwnProfile = settingsStore.player?.id == playerId;
|
||||||
|
const toastId: any = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted || isOwnProfile) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function claimProfile() {
|
||||||
|
settingsStore?.setProfile(playerData);
|
||||||
|
addProfile(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addFriend() {
|
||||||
|
const friend = await settingsStore?.addFriend(playerData.id);
|
||||||
|
if (!friend) {
|
||||||
|
toast.error(`Failed to add ${playerData.name} as a friend`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addProfile(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeFriend() {
|
||||||
|
settingsStore?.removeFriend(playerData.id);
|
||||||
|
|
||||||
|
toast.success(`Successfully removed ${playerData.name} as a friend`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addProfile(isFriend: boolean) {
|
||||||
|
if (!useScoresaberScoresStore.getState().exists(playerId)) {
|
||||||
|
if (!isFriend) {
|
||||||
|
toast.success(`Successfully set ${playerData.name} as your profile`);
|
||||||
|
} else {
|
||||||
|
toast.success(`Successfully added ${playerData.name} as a friend`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reponse = await playerScoreStore?.addOrUpdatePlayer(
|
||||||
|
playerId,
|
||||||
|
(page, totalPages) => {
|
||||||
|
const autoClose = page == totalPages ? 5000 : false;
|
||||||
|
|
||||||
|
if (page == 1) {
|
||||||
|
toastId.current = toast.info(
|
||||||
|
`Fetching scores for ${playerData.name} page ${page}/${totalPages}`,
|
||||||
|
{
|
||||||
|
autoClose: autoClose,
|
||||||
|
progress: page / totalPages,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (page != totalPages) {
|
||||||
|
toast.update(toastId.current, {
|
||||||
|
progress: page / totalPages,
|
||||||
|
render: `Fetching scores for ${playerData.name} page ${page}/${totalPages}`,
|
||||||
|
autoClose: autoClose,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.update(toastId.current, {
|
||||||
|
progress: 0,
|
||||||
|
render: `Successfully fetched scores for ${playerData.name}`,
|
||||||
|
autoClose: autoClose,
|
||||||
|
type: "success",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Fetching scores for ${playerId} (${page}/${totalPages})`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (reponse?.error) {
|
||||||
|
toast.error("Failed to fetch scores");
|
||||||
|
console.log(reponse.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={claimProfile}
|
||||||
|
tooltip={<p>Set as your Profile</p>}
|
||||||
|
icon={<HomeIcon width={24} height={24} />}
|
||||||
|
ariaLabel="Set as your Profile"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!settingsStore?.isFriend(playerId) && (
|
||||||
|
<Button
|
||||||
|
onClick={addFriend}
|
||||||
|
tooltip={<p>Add as Friend</p>}
|
||||||
|
icon={<UserIcon width={24} height={24} />}
|
||||||
|
color="bg-green-500"
|
||||||
|
ariaLabel="Add Friend"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{settingsStore.isFriend(playerId) && (
|
||||||
|
<Button
|
||||||
|
onClick={removeFriend}
|
||||||
|
tooltip={<p>Remove Friend</p>}
|
||||||
|
icon={<XMarkIcon width={24} height={24} />}
|
||||||
|
color="bg-red-500"
|
||||||
|
ariaLabel="Remove Friend"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user