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

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

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