This commit is contained in:
parent
51ecd67747
commit
70c5109417
21
package-lock.json
generated
21
package-lock.json
generated
@ -22,6 +22,7 @@
|
|||||||
"react-chartjs-2": "^5.2.0",
|
"react-chartjs-2": "^5.2.0",
|
||||||
"react-country-flag": "^3.1.0",
|
"react-country-flag": "^3.1.0",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"react-toastify": "^9.1.3",
|
||||||
"sharp": "^0.32.6",
|
"sharp": "^0.32.6",
|
||||||
"zustand": "^4.4.3"
|
"zustand": "^4.4.3"
|
||||||
},
|
},
|
||||||
@ -4635,6 +4636,26 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/react-toastify": {
|
||||||
|
"version": "9.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.3.tgz",
|
||||||
|
"integrity": "sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==",
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16",
|
||||||
|
"react-dom": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-toastify/node_modules/clsx": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
"react-chartjs-2": "^5.2.0",
|
"react-chartjs-2": "^5.2.0",
|
||||||
"react-country-flag": "^3.1.0",
|
"react-country-flag": "^3.1.0",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"react-toastify": "^9.1.3",
|
||||||
"sharp": "^0.32.6",
|
"sharp": "^0.32.6",
|
||||||
"zustand": "^4.4.3"
|
"zustand": "^4.4.3"
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import "react-toastify/dist/ReactToastify.css";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
const font = Inter({ subsets: ["latin-ext"], weight: "500" });
|
const font = Inter({ subsets: ["latin-ext"], weight: "500" });
|
||||||
|
@ -10,17 +10,20 @@ import Score from "@/components/Score";
|
|||||||
import { Spinner } from "@/components/Spinner";
|
import { Spinner } from "@/components/Spinner";
|
||||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||||
import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
|
import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
|
||||||
|
import { useSettingsStore } from "@/store/settingsStore";
|
||||||
import { formatNumber } from "@/utils/number";
|
import { formatNumber } from "@/utils/number";
|
||||||
import { fetchScores, getPlayerInfo } from "@/utils/scoresaber/api";
|
import { fetchScores, getPlayerInfo } from "@/utils/scoresaber/api";
|
||||||
import {
|
import {
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
GlobeAsiaAustraliaIcon,
|
GlobeAsiaAustraliaIcon,
|
||||||
|
HomeIcon,
|
||||||
TrophyIcon,
|
TrophyIcon,
|
||||||
} from "@heroicons/react/20/solid";
|
} from "@heroicons/react/20/solid";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import ReactCountryFlag from "react-country-flag";
|
import ReactCountryFlag from "react-country-flag";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
type PageInfo = {
|
type PageInfo = {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@ -56,6 +59,7 @@ const sortTypes: { [key: string]: SortType } = {
|
|||||||
const DEFAULT_SORT_TYPE = sortTypes.top;
|
const DEFAULT_SORT_TYPE = sortTypes.top;
|
||||||
|
|
||||||
export default function Player({ params }: { params: { id: string } }) {
|
export default function Player({ params }: { params: { id: string } }) {
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@ -70,6 +74,7 @@ export default function Player({ params }: { params: { id: string } }) {
|
|||||||
let sortType;
|
let sortType;
|
||||||
const sortTypeString = searchParams.get("sort");
|
const sortTypeString = searchParams.get("sort");
|
||||||
if (sortTypeString == null) {
|
if (sortTypeString == null) {
|
||||||
|
// todo: check settings to get last used sort type
|
||||||
sortType = DEFAULT_SORT_TYPE;
|
sortType = DEFAULT_SORT_TYPE;
|
||||||
} else {
|
} else {
|
||||||
sortType = sortTypes[sortTypeString] || DEFAULT_SORT_TYPE;
|
sortType = sortTypes[sortTypeString] || DEFAULT_SORT_TYPE;
|
||||||
@ -129,6 +134,11 @@ export default function Player({ params }: { params: { id: string } }) {
|
|||||||
[params.id, router, scores],
|
[params.id, router, scores],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function claimProfile() {
|
||||||
|
settingsStore.setUserId(params.id);
|
||||||
|
toast.success("Successfully claimed profile");
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!params.id) {
|
if (!params.id) {
|
||||||
setError(true);
|
setError(true);
|
||||||
@ -187,13 +197,29 @@ export default function Player({ params }: { params: { id: string } }) {
|
|||||||
<Container>
|
<Container>
|
||||||
{/* Player Info */}
|
{/* Player Info */}
|
||||||
<Card className="mt-2">
|
<Card className="mt-2">
|
||||||
<div className="flex flex-col items-center gap-3 xs:flex-row xs:items-start">
|
<div className="flex flex-col items-center gap-3 md:flex-row md:items-start">
|
||||||
<div>
|
<div className="min-w-fit">
|
||||||
|
{/* Avatar */}
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<Avatar url={playerData.profilePicture} label="Avatar" />
|
<Avatar url={playerData.profilePicture} label="Avatar" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Settings Buttons */}
|
||||||
|
<div className="absolute right-3 top-16 flex justify-end md:relative md:right-3 md:top-0 md:mt-2 md:justify-center">
|
||||||
|
{settingsStore.userId !== params.id && (
|
||||||
|
<button>
|
||||||
|
<HomeIcon
|
||||||
|
title="Set as your Profile"
|
||||||
|
width={28}
|
||||||
|
height={28}
|
||||||
|
onClick={claimProfile}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 flex w-full flex-col items-center gap-2 xs:items-start">
|
<div className="mt-1 flex w-full flex-col items-center gap-2 md:items-start">
|
||||||
|
{/* Name */}
|
||||||
<p className="text-2xl">{playerData.name}</p>
|
<p className="text-2xl">{playerData.name}</p>
|
||||||
|
|
||||||
<div className="flex gap-3 text-xl">
|
<div className="flex gap-3 text-xl">
|
||||||
@ -219,7 +245,7 @@ export default function Player({ params }: { params: { id: string } }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Labels */}
|
{/* Labels */}
|
||||||
<div className="flex flex-wrap justify-center gap-2 xs:justify-start">
|
<div className="flex flex-wrap justify-center gap-2 md:justify-start">
|
||||||
<Label
|
<Label
|
||||||
title="Total play count"
|
title="Total play count"
|
||||||
className="bg-blue-500"
|
className="bg-blue-500"
|
||||||
@ -249,7 +275,7 @@ export default function Player({ params }: { params: { id: string } }) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Scores */}
|
{/* Scores */}
|
||||||
<Card className="mt-2 w-full items-center xs:flex-col">
|
<Card className="mt-2 w-full items-center md:flex-col">
|
||||||
{/* Sort */}
|
{/* Sort */}
|
||||||
<div className="m-2 w-full text-sm">
|
<div className="m-2 w-full text-sm">
|
||||||
<div className="flex justify-center gap-2">
|
<div className="flex justify-center gap-2">
|
||||||
@ -297,7 +323,7 @@ export default function Player({ params }: { params: { id: string } }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<div className="flex w-full flex-row justify-center rounded-md bg-gray-800 xs:flex-col">
|
<div className="flex w-full flex-row justify-center rounded-md bg-gray-800 md:flex-col">
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={scores.page}
|
currentPage={scores.page}
|
||||||
|
@ -164,7 +164,7 @@ export default function Player({ params }: { params: { id: string } }) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Pagination */}
|
{/* Pagination */}
|
||||||
<div className="flex w-full flex-row justify-center rounded-md bg-gray-800 xs:flex-col">
|
<div className="flex w-full flex-row justify-center rounded-md bg-gray-800 md:flex-col">
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
<Pagination
|
<Pagination
|
||||||
currentPage={pageInfo.page}
|
currentPage={pageInfo.page}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
|
import { ToastContainer } from "react-toastify";
|
||||||
import Footer from "./Footer";
|
import Footer from "./Footer";
|
||||||
import Navbar from "./Navbar";
|
import Navbar from "./Navbar";
|
||||||
|
|
||||||
export default function Container({ children }: { children: React.ReactNode }) {
|
export default function Container({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<ToastContainer className="z-50" position="top-right" theme="dark" />
|
||||||
<div className="m-auto flex h-screen min-h-full flex-col items-center opacity-90 md:max-w-[1200px]">
|
<div className="m-auto flex h-screen min-h-full flex-col items-center opacity-90 md:max-w-[1200px]">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div className="w-full flex-1">{children}</div>
|
<div className="w-full flex-1">{children}</div>
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useSettingsStore } from "@/store/settingsStore";
|
||||||
|
import { useStore } from "@/utils/useState";
|
||||||
import {
|
import {
|
||||||
CogIcon,
|
CogIcon,
|
||||||
MagnifyingGlassIcon,
|
MagnifyingGlassIcon,
|
||||||
UserIcon,
|
UserIcon,
|
||||||
} from "@heroicons/react/20/solid";
|
} from "@heroicons/react/20/solid";
|
||||||
import { GlobeAltIcon } from "@heroicons/react/24/outline";
|
import { GlobeAltIcon } from "@heroicons/react/24/outline";
|
||||||
|
import Avatar from "./Avatar";
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
|
|
||||||
interface ButtonProps {
|
interface ButtonProps {
|
||||||
@ -17,12 +22,12 @@ function NavbarButton({ text, icon, href, children }: ButtonProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="group">
|
<div className="group">
|
||||||
<a
|
<a
|
||||||
className="flex w-fit transform-gpu items-center justify-center gap-1 rounded-md p-3 transition-all hover:cursor-pointer hover:bg-blue-500"
|
className="flex h-full w-fit transform-gpu items-center justify-center gap-1 rounded-md p-3 transition-all hover:cursor-pointer hover:bg-blue-500"
|
||||||
href={href}
|
href={href}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{icon}
|
{icon}
|
||||||
<p className="hidden xs:block">{text}</p>
|
<p className="hidden md:block">{text}</p>
|
||||||
</>
|
</>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@ -36,9 +41,30 @@ function NavbarButton({ text, icon, href, children }: ButtonProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
|
const settingsStore = useStore(useSettingsStore, (state) => {
|
||||||
|
return {
|
||||||
|
profilePicture: state.profilePicture,
|
||||||
|
userId: state.userId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex h-fit w-full rounded-md bg-gray-800">
|
<div className="flex h-fit w-full rounded-md bg-gray-800">
|
||||||
|
{settingsStore && settingsStore.profilePicture && (
|
||||||
|
<NavbarButton
|
||||||
|
text="You"
|
||||||
|
icon={
|
||||||
|
<Avatar
|
||||||
|
url={settingsStore.profilePicture}
|
||||||
|
label="Your avatar"
|
||||||
|
size={32}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
href={`/player/${settingsStore.userId}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<NavbarButton text="Friends" icon={<UserIcon height={20} width={20} />}>
|
<NavbarButton text="Friends" icon={<UserIcon height={20} width={20} />}>
|
||||||
<p className="text-sm font-bold">No friends, add someone!</p>
|
<p className="text-sm font-bold">No friends, add someone!</p>
|
||||||
|
|
||||||
|
@ -16,13 +16,13 @@ export default function Score({ score, leaderboard }: ScoreProps) {
|
|||||||
const isFullCombo = score.missedNotes + score.badCuts === 0;
|
const isFullCombo = score.missedNotes + score.badCuts === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 pb-2 pt-2 first:pt-0 last:pb-0 md:grid-cols-[1.1fr_6fr_3fr] xl:xs:grid-cols-[.95fr_6fr_3fr]">
|
<div className="grid grid-cols-1 pb-2 pt-2 first:pt-0 last:pb-0 md:grid-cols-[1.1fr_6fr_3fr] xl:md:grid-cols-[.95fr_6fr_3fr]">
|
||||||
<div className="ml-3 flex flex-col items-start justify-center">
|
<div className="ml-3 flex flex-col items-start justify-center">
|
||||||
<div className="hidden w-fit flex-row items-center justify-start gap-1 md:flex">
|
<div className="hidden w-fit flex-row items-center justify-start gap-1 md:flex">
|
||||||
<GlobeAsiaAustraliaIcon width={20} height={20} />
|
<GlobeAsiaAustraliaIcon width={20} height={20} />
|
||||||
<p>#{score.rank}</p>
|
<p>#{score.rank}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="hidden text-sm text-gray-200 xs:block">
|
<p className="hidden text-sm text-gray-200 md:block">
|
||||||
{moment(score.timeSet).fromNow()}
|
{moment(score.timeSet).fromNow()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -55,7 +55,7 @@ export default function Score({ score, leaderboard }: ScoreProps) {
|
|||||||
{/* Time Set (Mobile) */}
|
{/* Time Set (Mobile) */}
|
||||||
<div>
|
<div>
|
||||||
{" "}
|
{" "}
|
||||||
<p className="block text-sm text-gray-200 xs:hidden">
|
<p className="block text-sm text-gray-200 md:hidden">
|
||||||
{moment(score.timeSet).fromNow()}
|
{moment(score.timeSet).fromNow()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,19 +1,47 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { getPlayerInfo } from "@/utils/scoresaber/api";
|
||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { createJSONStorage, persist } from "zustand/middleware";
|
import { persist } from "zustand/middleware";
|
||||||
|
|
||||||
export const useSettingsStore = create(
|
interface SettingsStore {
|
||||||
|
userId: string | undefined;
|
||||||
|
profilePicture: string | undefined;
|
||||||
|
|
||||||
|
setUserId: (userId: string) => void;
|
||||||
|
setProfilePicture: (profilePicture: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSettingsStore = create<SettingsStore>()(
|
||||||
persist(
|
persist(
|
||||||
(set: any, get: any) => ({
|
(set) => ({
|
||||||
userId: null,
|
userId: undefined,
|
||||||
|
profilePicture: undefined,
|
||||||
|
|
||||||
setUserId: (userId: string) => set({ userId: userId }),
|
setUserId: (userId: string) => {
|
||||||
|
set({ userId });
|
||||||
|
},
|
||||||
|
setProfilePicture: (profilePicture: string) => set({ profilePicture }),
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "settings", // name of the item in the storage (must be unique)
|
name: "settings",
|
||||||
storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
|
getStorage: () => localStorage,
|
||||||
skipHydration: true,
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
async function refreshProfile() {
|
||||||
|
const id = useSettingsStore.getState().userId;
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
const profile = await getPlayerInfo(id);
|
||||||
|
if (profile == undefined || profile == null) return;
|
||||||
|
|
||||||
|
useSettingsStore.setState({
|
||||||
|
userId: profile.id,
|
||||||
|
profilePicture: profile.profilePicture,
|
||||||
|
});
|
||||||
|
console.log("Updated profile:", profile.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshProfile();
|
||||||
|
15
src/utils/useState.ts
Normal file
15
src/utils/useState.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export const useStore = <T, F>(
|
||||||
|
store: (callback: (state: T) => unknown) => unknown,
|
||||||
|
callback: (state: T) => F,
|
||||||
|
) => {
|
||||||
|
const result = store(callback) as F;
|
||||||
|
const [data, setData] = useState<F>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setData(result);
|
||||||
|
}, [result]);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
@ -17,7 +17,6 @@ const config: Config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
screens: {
|
screens: {
|
||||||
xs: "545px",
|
|
||||||
...defaultTheme.screens,
|
...defaultTheme.screens,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user