added profile claiming
All checks were successful
deploy / deploy (push) Successful in 2m8s

This commit is contained in:
Lee 2023-10-21 22:16:46 +01:00
parent 51ecd67747
commit 70c5109417
11 changed files with 140 additions and 21 deletions

21
package-lock.json generated
View File

@ -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",

View File

@ -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"
},

View File

@ -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" });

View File

@ -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}

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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
View 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;
};

View File

@ -17,7 +17,6 @@ const config: Config = {
},
},
screens: {
xs: "545px",
...defaultTheme.screens,
},
},