diff --git a/package.json b/package.json index f41a666..35abf77 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-tooltip": "^1.1.2", + "@tanstack/react-query": "^5.55.4", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "dexie": "^4.0.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 252ec2b..316c12a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,9 @@ dependencies: '@radix-ui/react-tooltip': specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@18.3.1)(react@18.3.1) + '@tanstack/react-query': + specifier: ^5.55.4 + version: 5.55.4(react@18.3.1) class-variance-authority: specifier: ^0.7.0 version: 0.7.0 @@ -872,6 +875,19 @@ packages: tslib: 2.7.0 dev: false + /@tanstack/query-core@5.55.4: + resolution: {integrity: sha512-uoRqNnRfzOH4OMIoxj8E2+Us89UIGXfau981qYJWsNMkFS1GXR4UIyzUTVGq4N7SDLHgFPpo6IOazqUV5gkMZA==} + dev: false + + /@tanstack/react-query@5.55.4(react@18.3.1): + resolution: {integrity: sha512-e3uX5XkLD9oTV66/VsVpkYz3Ds/ps/Yk+V5d89xthAbtNIKKBEm4FdNb9yISFzGEGezUzVO68qmfmiSrtScvsg==} + peerDependencies: + react: ^18 || ^19 + dependencies: + '@tanstack/query-core': 5.55.4 + react: 18.3.1 + dev: false + /@types/json-schema@7.0.15: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true diff --git a/src/app/(pages)/page.tsx b/src/app/(pages)/page.tsx index 3abd21a..607f00a 100644 --- a/src/app/(pages)/page.tsx +++ b/src/app/(pages)/page.tsx @@ -1,10 +1,5 @@ "use client"; -import { useLiveQuery } from "dexie-react-hooks"; -import useDatabase from "../hooks/use-database"; - export default function Home() { - const database = useDatabase(); - const settings = useLiveQuery(() => database.getSettings()); - return <>{settings?.playerId}; + return <>home page; } diff --git a/src/app/(pages)/player/[...slug]/page.tsx b/src/app/(pages)/player/[...slug]/page.tsx index 0970025..54a92ef 100644 --- a/src/app/(pages)/player/[...slug]/page.tsx +++ b/src/app/(pages)/player/[...slug]/page.tsx @@ -1,38 +1,10 @@ import { scoresaberLeaderboard } from "@/app/common/leaderboard/impl/scoresaber"; import { ScoreSort } from "@/app/common/leaderboard/sort"; -import ScoreSaberPlayer from "@/app/common/leaderboard/types/scoresaber/scoresaber-player"; import { formatNumberWithCommas } from "@/app/common/number-utils"; -import CountryFlag from "@/app/components/country-flag"; -import ClaimProfile from "@/app/components/player/claim-profile"; -import PlayerSubName from "@/app/components/player/player-sub-name"; -import { Avatar, AvatarFallback, AvatarImage } from "@/app/components/ui/avatar"; +import PlayerData from "@/app/components/player/player-data"; import { format } from "@formkit/tempo"; -import { GlobeAmericasIcon } from "@heroicons/react/24/solid"; import { Metadata } from "next"; - -const playerSubNames = [ - { - icon: () => { - return ; - }, - render: (player: ScoreSaberPlayer) => { - return

#{formatNumberWithCommas(player.rank)}

; - }, - }, - { - icon: (player: ScoreSaberPlayer) => { - return ; - }, - render: (player: ScoreSaberPlayer) => { - return

#{formatNumberWithCommas(player.countryRank)}

; - }, - }, - { - render: (player: ScoreSaberPlayer) => { - return

{formatNumberWithCommas(player.pp)}pp

; - }, - }, -]; +import { redirect } from "next/navigation"; type Props = { params: { @@ -76,49 +48,13 @@ export default async function Search({ params: { slug } }: Props) { console.log("sort", sort); console.log("page", page); + if (player == undefined) { + return redirect("/"); + } + return (
- {player === undefined && ( -
-

idek mate

-
- )} - {player !== undefined && ( -
-
-
- - - {player.name} - -
-

{player.name}

-
- {playerSubNames.map((subName, index) => { - return ( - - {subName.render(player)} - - ); - })} - {/* }> -

#{formatNumberWithCommas(player.rank)}

-
- }> -

#{formatNumberWithCommas(player.countryRank)}

-
- -

{formatNumberWithCommas(player.pp)}pp

-
*/} -
-
- -
-
-
-
-
- )} +
); } diff --git a/src/app/common/leaderboard/impl/scoresaber.ts b/src/app/common/leaderboard/impl/scoresaber.ts index 23a089f..bc83917 100644 --- a/src/app/common/leaderboard/impl/scoresaber.ts +++ b/src/app/common/leaderboard/impl/scoresaber.ts @@ -16,6 +16,7 @@ class ScoreSaberLeaderboard extends Leaderboard { * @returns the players that match the query, or undefined if no players were found */ async searchPlayers(query: string, useProxy = true): Promise { + console.log(`SS API: Searching for players matching "${query}"...`); try { const results = await ky .get((useProxy ? "https://proxy.fascinated.cc/" : "") + SEARCH_PLAYERS_ENDPOINT.replace("{query}", query)) @@ -38,6 +39,7 @@ class ScoreSaberLeaderboard extends Leaderboard { * @returns the player that matches the ID, or undefined */ async lookupPlayer(playerId: string, useProxy = true): Promise { + console.log(`SS API: Looking up player "${playerId}"...`); try { const results = await ky .get((useProxy ? "https://proxy.fascinated.cc/" : "") + LOOKUP_PLAYER_ENDPOINT.replace("{playerId}", playerId)) diff --git a/src/app/components/country-flag.tsx b/src/app/components/country-flag.tsx index f68c0ec..a277951 100644 --- a/src/app/components/country-flag.tsx +++ b/src/app/components/country-flag.tsx @@ -6,6 +6,6 @@ type Props = { export default function CountryFlag({ country, size = 24 }: Props) { return ( // eslint-disable-next-line @next/next/no-img-element - Player Country + Player Country ); } diff --git a/src/app/components/input/search-player.tsx b/src/app/components/input/search-player.tsx index 395f8ae..94ddefd 100644 --- a/src/app/components/input/search-player.tsx +++ b/src/app/components/input/search-player.tsx @@ -69,7 +69,7 @@ export default function SearchPlayer() { {results?.map((player) => { return ( diff --git a/src/app/components/navbar/navbar-button.tsx b/src/app/components/navbar/navbar-button.tsx new file mode 100644 index 0000000..1acabba --- /dev/null +++ b/src/app/components/navbar/navbar-button.tsx @@ -0,0 +1,11 @@ +type Props = { + children: React.ReactNode; +}; + +export default function NavbarButton({ children }: Props) { + return ( +
+ {children} +
+ ); +} diff --git a/src/app/components/navbar.tsx b/src/app/components/navbar/navbar.tsx similarity index 59% rename from src/app/components/navbar.tsx rename to src/app/components/navbar/navbar.tsx index 652388f..541334d 100644 --- a/src/app/components/navbar.tsx +++ b/src/app/components/navbar/navbar.tsx @@ -1,5 +1,9 @@ +import { HomeIcon } from "@heroicons/react/24/solid"; +import { MagnifyingGlassIcon } from "@radix-ui/react-icons"; import Link from "next/link"; import React from "react"; +import NavbarButton from "./navbar-button"; +import ProfileButton from "./profile-button"; type NavbarItem = { name: string; @@ -11,12 +15,12 @@ const items: NavbarItem[] = [ { name: "Home", link: "/", - icon: undefined, // Add your home icon here + icon: , // Add your home icon here }, { name: "Search", link: "/search", - icon: undefined, // Add your search icon here + icon: , // Add your search icon here }, ]; @@ -31,20 +35,22 @@ const renderNavbarItem = (item: NavbarItem) => ( export default function Navbar() { return (
-
+
{/* Left-aligned items */} -
+
+ + {items.slice(0, -1).map((item, index) => ( -
+ {renderNavbarItem(item)} -
+ ))}
{/* Right-aligned item */} -
+ {renderNavbarItem(items[items.length - 1])} -
+
); diff --git a/src/app/components/navbar/profile-button.tsx b/src/app/components/navbar/profile-button.tsx new file mode 100644 index 0000000..47a54c8 --- /dev/null +++ b/src/app/components/navbar/profile-button.tsx @@ -0,0 +1,27 @@ +"use client"; + +import useDatabase from "@/app/hooks/use-database"; +import { useLiveQuery } from "dexie-react-hooks"; +import Link from "next/link"; +import { Avatar, AvatarImage } from "../ui/avatar"; +import NavbarButton from "./navbar-button"; + +export default function ProfileButton() { + const database = useDatabase(); + const settings = useLiveQuery(() => database.getSettings()); + + if (settings == undefined) { + return; // Settings hasn't loaded yet + } + + return ( + + + + + +

You

+ +
+ ); +} diff --git a/src/app/components/player/player-data.tsx b/src/app/components/player/player-data.tsx new file mode 100644 index 0000000..a3afc49 --- /dev/null +++ b/src/app/components/player/player-data.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { scoresaberLeaderboard } from "@/app/common/leaderboard/impl/scoresaber"; +import ScoreSaberPlayer from "@/app/common/leaderboard/types/scoresaber/scoresaber-player"; +import { useQuery } from "@tanstack/react-query"; +import PlayerHeader from "./player-header"; + +const REFRESH_INTERVAL = 5 * 60 * 1000; // 5 minutes + +type Props = { + initalPlayerData: ScoreSaberPlayer; +}; + +export default function PlayerData({ initalPlayerData }: Props) { + let player = initalPlayerData; + const { data, isLoading, isError } = useQuery({ + queryKey: ["player", player.id], + queryFn: () => scoresaberLeaderboard.lookupPlayer(player.id), + refetchInterval: REFRESH_INTERVAL, + }); + + if (data && (!isLoading || !isError)) { + player = data; + } + + return ( +
+ +
+ ); +} diff --git a/src/app/components/player/player-header.tsx b/src/app/components/player/player-header.tsx new file mode 100644 index 0000000..918e0f2 --- /dev/null +++ b/src/app/components/player/player-header.tsx @@ -0,0 +1,62 @@ +import ScoreSaberPlayer from "@/app/common/leaderboard/types/scoresaber/scoresaber-player"; +import { formatNumberWithCommas } from "@/app/common/number-utils"; +import { GlobeAmericasIcon } from "@heroicons/react/24/solid"; +import CountryFlag from "../country-flag"; +import { Avatar, AvatarImage } from "../ui/avatar"; +import ClaimProfile from "./claim-profile"; +import PlayerSubName from "./player-sub-name"; + +const playerSubNames = [ + { + icon: () => { + return ; + }, + render: (player: ScoreSaberPlayer) => { + return

#{formatNumberWithCommas(player.rank)}

; + }, + }, + { + icon: (player: ScoreSaberPlayer) => { + return ; + }, + render: (player: ScoreSaberPlayer) => { + return

#{formatNumberWithCommas(player.countryRank)}

; + }, + }, + { + render: (player: ScoreSaberPlayer) => { + return

{formatNumberWithCommas(player.pp)}pp

; + }, + }, +]; + +type Props = { + player: ScoreSaberPlayer; +}; + +export default function PlayerHeader({ player }: Props) { + return ( +
+
+ + + +
+

{player.name}

+
+ {playerSubNames.map((subName, index) => { + return ( + + {subName.render(player)} + + ); + })} +
+
+ +
+
+
+
+ ); +} diff --git a/src/app/components/providers/query-provider.tsx b/src/app/components/providers/query-provider.tsx new file mode 100644 index 0000000..c19f40e --- /dev/null +++ b/src/app/components/providers/query-provider.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +type Props = { + children: React.ReactNode; +}; + +const queryClient = new QueryClient(); + +export function QueryProvider({ children }: Props) { + return {children}; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 82fed7e..8b7ef48 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -2,7 +2,8 @@ import type { Metadata } from "next"; import localFont from "next/font/local"; import BackgroundImage from "./components/background-image"; import DatabaseLoader from "./components/loaders/database-loader"; -import NavBar from "./components/navbar"; +import NavBar from "./components/navbar/navbar"; +import { QueryProvider } from "./components/providers/query-provider"; import { ThemeProvider } from "./components/providers/theme-provider"; import { Toaster } from "./components/ui/toaster"; import { TooltipProvider } from "./components/ui/tooltip"; @@ -63,10 +64,12 @@ export default function RootLayout({ -
- - {children} -
+ +
+ + {children} +
+
diff --git a/src/middleware.ts b/src/middleware.ts deleted file mode 100644 index 28b1e81..0000000 --- a/src/middleware.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { NextRequest } from "next/server"; -import { NextResponse } from "next/server"; - -export function middleware(request: NextRequest) { - const { pathname } = request.nextUrl; - const cookies = request.cookies; - - const playerIdCookie = cookies.get("playerId"); - if (pathname == "/") { - if (playerIdCookie) { - return NextResponse.redirect(new URL(`/player/${playerIdCookie.value}`, request.url)); - } else { - return NextResponse.redirect(new URL("/search", request.url)); - } - } -} - -export const config = { - matcher: "/((?!api|_next/static|_next/image|favicon.ico).*)", -};