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-country-flag": "^3.1.0",
|
||||
"react-dom": "^18",
|
||||
"react-toastify": "^9.1.3",
|
||||
"sharp": "^0.32.6",
|
||||
"zustand": "^4.4.3"
|
||||
},
|
||||
@ -4635,6 +4636,26 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
@ -23,6 +23,7 @@
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-country-flag": "^3.1.0",
|
||||
"react-dom": "^18",
|
||||
"react-toastify": "^9.1.3",
|
||||
"sharp": "^0.32.6",
|
||||
"zustand": "^4.4.3"
|
||||
},
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { Metadata } from "next";
|
||||
import { Inter } from "next/font/google";
|
||||
import Image from "next/image";
|
||||
import "react-toastify/dist/ReactToastify.css";
|
||||
import "./globals.css";
|
||||
|
||||
const font = Inter({ subsets: ["latin-ext"], weight: "500" });
|
||||
|
@ -10,17 +10,20 @@ import Score from "@/components/Score";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
import { ScoresaberPlayer } from "@/schemas/scoresaber/player";
|
||||
import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore";
|
||||
import { useSettingsStore } from "@/store/settingsStore";
|
||||
import { formatNumber } from "@/utils/number";
|
||||
import { fetchScores, getPlayerInfo } from "@/utils/scoresaber/api";
|
||||
import {
|
||||
ClockIcon,
|
||||
GlobeAsiaAustraliaIcon,
|
||||
HomeIcon,
|
||||
TrophyIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import Image from "next/image";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import ReactCountryFlag from "react-country-flag";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
type PageInfo = {
|
||||
loading: boolean;
|
||||
@ -56,6 +59,7 @@ const sortTypes: { [key: string]: SortType } = {
|
||||
const DEFAULT_SORT_TYPE = sortTypes.top;
|
||||
|
||||
export default function Player({ params }: { params: { id: string } }) {
|
||||
const settingsStore = useSettingsStore();
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
|
||||
@ -70,6 +74,7 @@ export default function Player({ params }: { params: { id: string } }) {
|
||||
let sortType;
|
||||
const sortTypeString = searchParams.get("sort");
|
||||
if (sortTypeString == null) {
|
||||
// todo: check settings to get last used sort type
|
||||
sortType = DEFAULT_SORT_TYPE;
|
||||
} else {
|
||||
sortType = sortTypes[sortTypeString] || DEFAULT_SORT_TYPE;
|
||||
@ -129,6 +134,11 @@ export default function Player({ params }: { params: { id: string } }) {
|
||||
[params.id, router, scores],
|
||||
);
|
||||
|
||||
function claimProfile() {
|
||||
settingsStore.setUserId(params.id);
|
||||
toast.success("Successfully claimed profile");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!params.id) {
|
||||
setError(true);
|
||||
@ -187,13 +197,29 @@ export default function Player({ params }: { params: { id: string } }) {
|
||||
<Container>
|
||||
{/* Player Info */}
|
||||
<Card className="mt-2">
|
||||
<div className="flex flex-col items-center gap-3 xs:flex-row xs:items-start">
|
||||
<div>
|
||||
<div className="flex flex-col items-center gap-3 md:flex-row md:items-start">
|
||||
<div className="min-w-fit">
|
||||
{/* Avatar */}
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<Avatar url={playerData.profilePicture} label="Avatar" />
|
||||
</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 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>
|
||||
|
||||
<div className="flex gap-3 text-xl">
|
||||
@ -219,7 +245,7 @@ export default function Player({ params }: { params: { id: string } }) {
|
||||
</div>
|
||||
</div>
|
||||
{/* 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
|
||||
title="Total play count"
|
||||
className="bg-blue-500"
|
||||
@ -249,7 +275,7 @@ export default function Player({ params }: { params: { id: string } }) {
|
||||
</Card>
|
||||
|
||||
{/* Scores */}
|
||||
<Card className="mt-2 w-full items-center xs:flex-col">
|
||||
<Card className="mt-2 w-full items-center md:flex-col">
|
||||
{/* Sort */}
|
||||
<div className="m-2 w-full text-sm">
|
||||
<div className="flex justify-center gap-2">
|
||||
@ -297,7 +323,7 @@ export default function Player({ params }: { params: { id: string } }) {
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<Pagination
|
||||
currentPage={scores.page}
|
||||
|
@ -164,7 +164,7 @@ export default function Player({ params }: { params: { id: string } }) {
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
<Pagination
|
||||
currentPage={pageInfo.page}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { ToastContainer } from "react-toastify";
|
||||
import Footer from "./Footer";
|
||||
import Navbar from "./Navbar";
|
||||
|
||||
export default function Container({ children }: { children: React.ReactNode }) {
|
||||
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]">
|
||||
<Navbar />
|
||||
<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 {
|
||||
CogIcon,
|
||||
MagnifyingGlassIcon,
|
||||
UserIcon,
|
||||
} from "@heroicons/react/20/solid";
|
||||
import { GlobeAltIcon } from "@heroicons/react/24/outline";
|
||||
import Avatar from "./Avatar";
|
||||
import Button from "./Button";
|
||||
|
||||
interface ButtonProps {
|
||||
@ -17,12 +22,12 @@ function NavbarButton({ text, icon, href, children }: ButtonProps) {
|
||||
return (
|
||||
<div className="group">
|
||||
<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}
|
||||
>
|
||||
<>
|
||||
{icon}
|
||||
<p className="hidden xs:block">{text}</p>
|
||||
<p className="hidden md:block">{text}</p>
|
||||
</>
|
||||
</a>
|
||||
|
||||
@ -36,9 +41,30 @@ function NavbarButton({ text, icon, href, children }: ButtonProps) {
|
||||
}
|
||||
|
||||
export default function Navbar() {
|
||||
const settingsStore = useStore(useSettingsStore, (state) => {
|
||||
return {
|
||||
profilePicture: state.profilePicture,
|
||||
userId: state.userId,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<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} />}>
|
||||
<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;
|
||||
|
||||
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="hidden w-fit flex-row items-center justify-start gap-1 md:flex">
|
||||
<GlobeAsiaAustraliaIcon width={20} height={20} />
|
||||
<p>#{score.rank}</p>
|
||||
</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()}
|
||||
</p>
|
||||
</div>
|
||||
@ -55,7 +55,7 @@ export default function Score({ score, leaderboard }: ScoreProps) {
|
||||
{/* Time Set (Mobile) */}
|
||||
<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()}
|
||||
</p>
|
||||
</div>
|
||||
|
@ -1,19 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { getPlayerInfo } from "@/utils/scoresaber/api";
|
||||
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(
|
||||
(set: any, get: any) => ({
|
||||
userId: null,
|
||||
(set) => ({
|
||||
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)
|
||||
storage: createJSONStorage(() => sessionStorage), // (optional) by default, 'localStorage' is used
|
||||
skipHydration: true,
|
||||
name: "settings",
|
||||
getStorage: () => localStorage,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
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: {
|
||||
xs: "545px",
|
||||
...defaultTheme.screens,
|
||||
},
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user