diff --git a/src/app/(pages)/api/proxy/route.ts b/src/app/(pages)/api/proxy/route.ts index edbec74..eb3bb86 100644 --- a/src/app/(pages)/api/proxy/route.ts +++ b/src/app/(pages)/api/proxy/route.ts @@ -21,7 +21,8 @@ export async function GET(request: NextRequest) { const { status, headers } = response; if ( !headers.has("content-type") || - (headers.has("content-type") && !headers.get("content-type")?.includes("application/json")) + (headers.has("content-type") && + !headers.get("content-type")?.includes("application/json")) ) { return NextResponse.json({ error: "We only support proxying JSON responses", @@ -41,7 +42,7 @@ export async function GET(request: NextRequest) { headers: { "Access-Control-Allow-Origin": "*", }, - } + }, ); } } diff --git a/src/app/(pages)/player/[...slug]/page.tsx b/src/app/(pages)/player/[...slug]/page.tsx index fad8e84..3274765 100644 --- a/src/app/(pages)/player/[...slug]/page.tsx +++ b/src/app/(pages)/player/[...slug]/page.tsx @@ -12,7 +12,9 @@ type Props = { }; }; -export async function generateMetadata({ params: { slug } }: Props): Promise { +export async function generateMetadata({ + params: { slug }, +}: Props): Promise { const id = slug[0]; // The players id const player = await scoresaberFetcher.lookupPlayer(id, false); if (player === undefined) { @@ -43,7 +45,11 @@ export default async function Search({ params: { slug } }: Props) { const sort: ScoreSort = (slug[1] as ScoreSort) || "recent"; // The sorting method const page = parseInt(slug[2]) || 1; // The page number const player = await scoresaberFetcher.lookupPlayer(id, false); - const scores = await scoresaberFetcher.lookupPlayerScores(id, sort, page); + const scores = await scoresaberFetcher.lookupPlayerScores({ + playerId: id, + sort, + page, + }); if (player == undefined) { // Invalid player id @@ -52,7 +58,12 @@ export default async function Search({ params: { slug } }: Props) { return (
- +
); } diff --git a/src/app/globals.css b/src/app/globals.css index f7fcb4d..3545eb7 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3,81 +3,82 @@ @tailwind utilities; :root { - --background: #ffffff; - --foreground: #171717; + --background: #ffffff; + --foreground: #171717; } @media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } + :root { + --background: #0a0a0a; + --foreground: #ededed; + } } body { - color: var(--foreground); - background: var(--background); - font-family: Arial, Helvetica, sans-serif; + color: var(--foreground); + background: var(--background); + font-family: Arial, Helvetica, sans-serif; } @layer utilities { - .text-balance { - text-wrap: balance; - } + .text-balance { + text-wrap: balance; + } } @layer base { - :root { - --background: 0 0% 100%; - --foreground: 20 14.3% 4.1%; - --card: 0 0% 100%; - --card-foreground: 20 14.3% 4.1%; - --popover: 0 0% 100%; - --popover-foreground: 20 14.3% 4.1%; - --primary: 24 9.8% 10%; - --primary-foreground: 60 9.1% 97.8%; - --secondary: 60 4.8% 95.9%; - --secondary-foreground: 24 9.8% 10%; - --muted: 60 4.8% 95.9%; - --muted-foreground: 25 5.3% 44.7%; - --accent: 60 4.8% 95.9%; - --accent-foreground: 24 9.8% 10%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 60 9.1% 97.8%; - --border: 20 5.9% 90%; - --input: 20 5.9% 90%; - --ring: 20 14.3% 4.1%; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --radius: 0.5rem; - } - .dark { - --background: 20 14.3% 4.1%; - --foreground: 60 9.1% 97.8%; - --card: 20 14.3% 4.1%; - --card-foreground: 60 9.1% 97.8%; - --popover: 20 14.3% 4.1%; - --popover-foreground: 60 9.1% 97.8%; - --primary: 60 9.1% 97.8%; - --primary-foreground: 24 9.8% 10%; - --secondary: 12 6.5% 9.5%; - --secondary-foreground: 60 9.1% 97.8%; - --muted: 12 6.5% 15.1%; - --muted-foreground: 24 5.4% 63.9%; - --accent: 12 6.5% 15.1%; - --accent-foreground: 60 9.1% 97.8%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 60 9.1% 97.8%; - --border: 12 6.5% 15.1%; - --input: 12 6.5% 45.1%; - --ring: 24 5.7% 82.9%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - } + :root { + --background: 0 0% 100%; + --foreground: 20 14.3% 4.1%; + --card: 0 0% 100%; + --card-foreground: 20 14.3% 4.1%; + --popover: 0 0% 100%; + --popover-foreground: 20 14.3% 4.1%; + --primary: 24 9.8% 10%; + --primary-foreground: 60 9.1% 97.8%; + --secondary: 60 4.8% 95.9%; + --secondary-foreground: 24 9.8% 10%; + --muted: 60 4.8% 95.9%; + --muted-foreground: 25 5.3% 44.7%; + --accent: 60 4.8% 95.9%; + --accent-foreground: 24 9.8% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 20 5.9% 90%; + --input: 20 5.9% 90%; + --ring: 20 14.3% 4.1%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + + .dark { + --background: 20 14.3% 4.1%; + --foreground: 60 9.1% 97.8%; + --card: 20 14.3% 4.1%; + --card-foreground: 60 9.1% 97.8%; + --popover: 20 14.3% 4.1%; + --popover-foreground: 60 9.1% 97.8%; + --primary: 60 9.1% 97.8%; + --primary-foreground: 24 9.8% 10%; + --secondary: 12 6.5% 9.5%; + --secondary-foreground: 60 9.1% 97.8%; + --muted: 12 6.5% 15.1%; + --muted-foreground: 24 5.4% 63.9%; + --accent: 12 6.5% 15.1%; + --accent-foreground: 60 9.1% 97.8%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 60 9.1% 97.8%; + --border: 12 6.5% 15.1%; + --input: 12 6.5% 45.1%; + --ring: 24 5.7% 82.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a6eb7f3..32ea760 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -45,12 +45,14 @@ export const metadata: Metadata = { "Stream enhancement, Professional overlay, Easy to use overlay builder.", openGraph: { title: "Scoresaber Reloaded", - description: "Scoresaber Reloaded is a new way to view your scores and get more stats about your and your plays", + description: + "Scoresaber Reloaded is a new way to view your scores and get more stats about your and your plays", url: "https://ssr.fascinated.cc", locale: "en_US", type: "website", }, - description: "Scoresaber Reloaded is a new way to view your scores and get more stats about your and your plays", + description: + "Scoresaber Reloaded is a new way to view your scores and get more stats about your and your plays", }; export default function RootLayout({ @@ -60,13 +62,20 @@ export default function RootLayout({ }>) { return ( - + - +
diff --git a/src/common/data-fetcher/data-fetcher.ts b/src/common/data-fetcher/data-fetcher.ts index d6b977f..e64abcd 100644 --- a/src/common/data-fetcher/data-fetcher.ts +++ b/src/common/data-fetcher/data-fetcher.ts @@ -38,7 +38,10 @@ export default class DataFetcher { * @param url the url to fetch * @returns the fetched data */ - public async fetch(useProxy: boolean, url: string): Promise { + public async fetch( + useProxy: boolean, + url: string, + ): Promise { try { return await ky .get(this.buildRequestUrl(useProxy, url), { diff --git a/src/common/data-fetcher/impl/beatsaver.ts b/src/common/data-fetcher/impl/beatsaver.ts index 933ea52..ef5182a 100644 --- a/src/common/data-fetcher/impl/beatsaver.ts +++ b/src/common/data-fetcher/impl/beatsaver.ts @@ -18,18 +18,26 @@ class BeatSaverFetcher extends DataFetcher { * @param useProxy whether to use the proxy or not * @returns the map that match the query, or undefined if no map were found */ - async lookupMap(query: string, useProxy = true): Promise { + async lookupMap( + query: string, + useProxy = true, + ): Promise { const before = performance.now(); this.log(`Looking up map "${query}"...`); let map = await db.beatSaverMaps.get(query); // The map is cached if (map != undefined) { - this.log(`Found cached map "${query}" in ${(performance.now() - before).toFixed(0)}ms`); + this.log( + `Found cached map "${query}" in ${(performance.now() - before).toFixed(0)}ms`, + ); return map; } - const response = await this.fetch(useProxy, LOOKUP_MAP_BY_HASH_ENDPOINT.replace(":query", query)); + const response = await this.fetch( + useProxy, + LOOKUP_MAP_BY_HASH_ENDPOINT.replace(":query", query), + ); // Map not found if (response == undefined) { return undefined; @@ -47,7 +55,9 @@ class BeatSaverFetcher extends DataFetcher { fullData: response, }); map = await db.beatSaverMaps.get(query); - this.log(`Found map "${query}" in ${(performance.now() - before).toFixed(0)}ms`); + this.log( + `Found map "${query}" in ${(performance.now() - before).toFixed(0)}ms`, + ); return map; } } diff --git a/src/common/data-fetcher/impl/scoresaber.ts b/src/common/data-fetcher/impl/scoresaber.ts index 9f01f2a..cb8e945 100644 --- a/src/common/data-fetcher/impl/scoresaber.ts +++ b/src/common/data-fetcher/impl/scoresaber.ts @@ -23,12 +23,15 @@ class ScoreSaberFetcher extends DataFetcher { * @param useProxy whether to use the proxy or not * @returns the players that match the query, or undefined if no players were found */ - async searchPlayers(query: string, useProxy = true): Promise { + async searchPlayers( + query: string, + useProxy = true, + ): Promise { const before = performance.now(); this.log(`Searching for players matching "${query}"...`); const results = await this.fetch( useProxy, - SEARCH_PLAYERS_ENDPOINT.replace(":query", query) + SEARCH_PLAYERS_ENDPOINT.replace(":query", query), ); if (results === undefined) { return undefined; @@ -37,7 +40,9 @@ class ScoreSaberFetcher extends DataFetcher { return undefined; } results.players.sort((a, b) => a.rank - b.rank); - this.log(`Found ${results.players.length} players in ${(performance.now() - before).toFixed(0)}ms`); + this.log( + `Found ${results.players.length} players in ${(performance.now() - before).toFixed(0)}ms`, + ); return results; } @@ -48,14 +53,22 @@ class ScoreSaberFetcher extends DataFetcher { * @param useProxy whether to use the proxy or not * @returns the player that matches the ID, or undefined */ - async lookupPlayer(playerId: string, useProxy = true): Promise { + async lookupPlayer( + playerId: string, + useProxy = true, + ): Promise { const before = performance.now(); this.log(`Looking up player "${playerId}"...`); - const response = await this.fetch(useProxy, LOOKUP_PLAYER_ENDPOINT.replace(":id", playerId)); + const response = await this.fetch( + useProxy, + LOOKUP_PLAYER_ENDPOINT.replace(":id", playerId), + ); if (response === undefined) { return undefined; } - this.log(`Found player "${playerId}" in ${(performance.now() - before).toFixed(0)}ms`); + this.log( + `Found player "${playerId}" in ${(performance.now() - before).toFixed(0)}ms`, + ); return response; } @@ -65,28 +78,40 @@ class ScoreSaberFetcher extends DataFetcher { * @param playerId the ID of the player to look up * @param sort the sort to use * @param page the page to get scores for + * @param search * @param useProxy whether to use the proxy or not * @returns the scores of the player, or undefined */ - async lookupPlayerScores( - playerId: string, - sort: ScoreSort, - page: number, - useProxy = true - ): Promise { + async lookupPlayerScores({ + playerId, + sort, + page, + search, + useProxy = true, + }: { + playerId: string; + sort: ScoreSort; + page: number; + search?: string; + useProxy?: boolean; + }): Promise { const before = performance.now(); - this.log(`Looking up scores for player "${playerId}", sort "${sort}", page "${page}"...`); + this.log( + `Looking up scores for player "${playerId}", sort "${sort}", page "${page}"${search ? `, search "${search}"` : ""}...`, + ); const response = await this.fetch( useProxy, LOOKUP_PLAYER_SCORES_ENDPOINT.replace(":id", playerId) .replace(":limit", 8 + "") .replace(":sort", sort) - .replace(":page", page.toString()) + .replace(":page", page + "") + (search ? `&search=${search}` : ""), ); if (response === undefined) { return undefined; } - this.log(`Found scores for player "${playerId}" in ${(performance.now() - before).toFixed(0)}ms`); + this.log( + `Found scores for player "${playerId}" in ${(performance.now() - before).toFixed(0)}ms`, + ); return response; } @@ -102,18 +127,25 @@ class ScoreSaberFetcher extends DataFetcher { async lookupLeaderboardScores( leaderboardId: string, page: number, - useProxy = true + useProxy = true, ): Promise { const before = performance.now(); - this.log(`Looking up scores for leaderboard "${leaderboardId}", page "${page}"...`); + this.log( + `Looking up scores for leaderboard "${leaderboardId}", page "${page}"...`, + ); const response = await this.fetch( useProxy, - LOOKUP_LEADERBOARD_SCORES_ENDPOINT.replace(":id", leaderboardId).replace(":page", page.toString()) + LOOKUP_LEADERBOARD_SCORES_ENDPOINT.replace(":id", leaderboardId).replace( + ":page", + page.toString(), + ), ); if (response === undefined) { return undefined; } - this.log(`Found scores for leaderboard "${leaderboardId}" in ${(performance.now() - before).toFixed(0)}ms`); + this.log( + `Found scores for leaderboard "${leaderboardId}" in ${(performance.now() - before).toFixed(0)}ms`, + ); return response; } } diff --git a/src/common/time-utils.ts b/src/common/time-utils.ts index 542fa7d..28203fc 100644 --- a/src/common/time-utils.ts +++ b/src/common/time-utils.ts @@ -20,7 +20,10 @@ export function timeAgo(input: Date | number) { for (const key in ranges) { if (ranges[key] < Math.abs(secondsElapsed)) { const delta = secondsElapsed / ranges[key]; - return formatter.format(Math.round(delta), key as Intl.RelativeTimeFormatUnit); + return formatter.format( + Math.round(delta), + key as Intl.RelativeTimeFormatUnit, + ); } } } diff --git a/src/common/utils.ts b/src/common/utils.ts index 12ed29d..e6e733e 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -1,4 +1,4 @@ -import { clsx, type ClassValue } from "clsx"; +import { type ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { diff --git a/src/common/youtube-utils.ts b/src/common/youtube-utils.ts index 3c5203c..4e1d1f2 100644 --- a/src/common/youtube-utils.ts +++ b/src/common/youtube-utils.ts @@ -6,7 +6,11 @@ * @param author the author of the song * @returns the YouTube link for the song */ -export function songNameToYouTubeLink(name: string, songSubName: string, author: string) { +export function songNameToYouTubeLink( + name: string, + songSubName: string, + author: string, +) { const baseUrl = "https://www.youtube.com/results?search_query="; let query = ""; if (name) { diff --git a/src/components/background-image.tsx b/src/components/background-image.tsx index 02df072..e0b41c0 100644 --- a/src/components/background-image.tsx +++ b/src/components/background-image.tsx @@ -9,7 +9,10 @@ export default function BackgroundImage() { const database = useDatabase(); const settings = useLiveQuery(() => database.getSettings()); - if (settings?.backgroundImage == undefined || settings?.backgroundImage == "") { + if ( + settings?.backgroundImage == undefined || + settings?.backgroundImage == "" + ) { return null; // Don't render anything if the background image is not set } diff --git a/src/components/card.tsx b/src/components/card.tsx index 39bfb79..d20060b 100644 --- a/src/components/card.tsx +++ b/src/components/card.tsx @@ -6,5 +6,14 @@ type Props = { }; export default function Card({ children, className }: Props) { - return
{children}
; + return ( +
+ {children} +
+ ); } diff --git a/src/components/chart/customized-axis-tick.tsx b/src/components/chart/customized-axis-tick.tsx index 5c27a17..afaa488 100644 --- a/src/components/chart/customized-axis-tick.tsx +++ b/src/components/chart/customized-axis-tick.tsx @@ -12,7 +12,14 @@ export const CustomizedAxisTick = ({ }) => { return ( - + {payload.value} diff --git a/src/components/country-flag.tsx b/src/components/country-flag.tsx index a277951..52c0f59 100644 --- a/src/components/country-flag.tsx +++ b/src/components/country-flag.tsx @@ -6,6 +6,11 @@ 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/components/input/pagination.tsx b/src/components/input/pagination.tsx index 6845bd1..5e563dc 100644 --- a/src/components/input/pagination.tsx +++ b/src/components/input/pagination.tsx @@ -2,13 +2,13 @@ import { ArrowPathIcon } from "@heroicons/react/24/solid"; import clsx from "clsx"; import { useEffect, useState } from "react"; import { + Pagination as ShadCnPagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, - Pagination as ShadCnPagination, } from "../ui/pagination"; type PaginationItemWrapperProps = { @@ -23,7 +23,10 @@ type PaginationItemWrapperProps = { children: React.ReactNode; }; -function PaginationItemWrapper({ isLoadingPage, children }: PaginationItemWrapperProps) { +function PaginationItemWrapper({ + isLoadingPage, + children, +}: PaginationItemWrapperProps) { return ( void; }; -export default function Pagination({ mobilePagination, page, totalPages, loadingPage, onPageChange }: Props) { +export default function Pagination({ + mobilePagination, + page, + totalPages, + loadingPage, + onPageChange, +}: Props) { totalPages = Math.round(totalPages); const isLoading = loadingPage !== undefined; const [currentPage, setCurrentPage] = useState(page); @@ -72,7 +81,12 @@ export default function Pagination({ mobilePagination, page, totalPages, loading }, [page]); const handlePageChange = (newPage: number) => { - if (newPage < 1 || newPage > totalPages || newPage == currentPage || isLoading) { + if ( + newPage < 1 || + newPage > totalPages || + newPage == currentPage || + isLoading + ) { return; } @@ -95,15 +109,20 @@ export default function Pagination({ mobilePagination, page, totalPages, loading pageNumbers.push( <> - handlePageChange(1)}>1 + handlePageChange(1)}> + 1 + {/* Only show ellipsis if more than 2 pages from the start */} {startPage > 2 && ( - + )} - + , ); } @@ -111,10 +130,17 @@ export default function Pagination({ mobilePagination, page, totalPages, loading for (let i = startPage; i <= endPage; i++) { pageNumbers.push( - handlePageChange(i)}> - {loadingPage === i ? : i} + handlePageChange(i)} + > + {loadingPage === i ? ( + + ) : ( + i + )} - + , ); } @@ -126,22 +152,31 @@ export default function Pagination({ mobilePagination, page, totalPages, loading {/* Previous button for mobile and desktop */} - handlePageChange(currentPage - 1)} /> + handlePageChange(currentPage - 1)} + /> {renderPageNumbers()} {/* For desktop, show ellipsis and link to the last page */} - {!mobilePagination && currentPage < totalPages && totalPages - currentPage > 2 && ( - <> - - - - - handlePageChange(totalPages)}>{totalPages} - - - )} + {!mobilePagination && + currentPage < totalPages && + totalPages - currentPage > 2 && ( + <> + + + + + handlePageChange(totalPages)}> + {totalPages} + + + + )} {/* Next button for mobile and desktop */} diff --git a/src/components/input/search-player.tsx b/src/components/input/search-player.tsx index 7117052..2a8c072 100644 --- a/src/components/input/search-player.tsx +++ b/src/components/input/search-player.tsx @@ -40,7 +40,10 @@ export default function SearchPlayer() {
{/* Search */}
- + Username - + )} @@ -79,7 +86,9 @@ export default function SearchPlayer() {

{player.name}

-

#{formatNumberWithCommas(player.rank)}

+

+ #{formatNumberWithCommas(player.rank)} +

); diff --git a/src/components/leaderboard/leaderboard-score-stats.tsx b/src/components/leaderboard/leaderboard-score-stats.tsx index dc77685..17e3fed 100644 --- a/src/components/leaderboard/leaderboard-score-stats.tsx +++ b/src/components/leaderboard/leaderboard-score-stats.tsx @@ -7,7 +7,10 @@ import clsx from "clsx"; type Badge = { name: string; - create: (score: ScoreSaberScore, leaderboard: ScoreSaberLeaderboard) => string | React.ReactNode | undefined; + create: ( + score: ScoreSaberScore, + leaderboard: ScoreSaberLeaderboard, + ) => string | React.ReactNode | undefined; }; const badges: Badge[] = [ @@ -35,8 +38,16 @@ const badges: Badge[] = [ return ( <> -

{fullCombo ? FC : formatNumberWithCommas(score.missedNotes)}

- +

+ {fullCombo ? ( + FC + ) : ( + formatNumberWithCommas(score.missedNotes) + )} +

+ ); }, diff --git a/src/components/leaderboard/leaderboard-scores.tsx b/src/components/leaderboard/leaderboard-scores.tsx index 19f1013..ee804b0 100644 --- a/src/components/leaderboard/leaderboard-scores.tsx +++ b/src/components/leaderboard/leaderboard-scores.tsx @@ -19,7 +19,9 @@ export default function LeaderboardScores({ leaderboard }: Props) { const { width } = useWindowDimensions(); const [currentPage, setCurrentPage] = useState(1); - const [currentScores, setCurrentScores] = useState(); + const [currentScores, setCurrentScores] = useState< + ScoreSaberLeaderboardScoresPage | undefined + >(); const { data: scores, @@ -28,7 +30,11 @@ export default function LeaderboardScores({ leaderboard }: Props) { refetch, } = useQuery({ queryKey: ["playerScores", leaderboard.id, currentPage], - queryFn: () => scoresaberFetcher.lookupLeaderboardScores(leaderboard.id + "", currentPage), + queryFn: () => + scoresaberFetcher.lookupLeaderboardScores( + leaderboard.id + "", + currentPage, + ), staleTime: 30 * 1000, // Cache data for 30 seconds }); @@ -47,23 +53,35 @@ export default function LeaderboardScores({ leaderboard }: Props) { } return ( - +
{isError &&

Oopsies! Something went wrong.

} - {currentScores.scores.length === 0 &&

No scores found. Invalid Page?

} + {currentScores.scores.length === 0 && ( +

No scores found. Invalid Page?

+ )}
{currentScores.scores.map((playerScore, index) => ( - + ))}
diff --git a/src/components/loaders/database-loader.tsx b/src/components/loaders/database-loader.tsx index 8f84d92..5a65387 100644 --- a/src/components/loaders/database-loader.tsx +++ b/src/components/loaders/database-loader.tsx @@ -24,7 +24,11 @@ export default function DatabaseLoader({ children }: Props) { return ( - {database == undefined ? : children} + {database == undefined ? ( + + ) : ( + children + )} ); } diff --git a/src/components/logos/beatsaver-logo.tsx b/src/components/logos/beatsaver-logo.tsx index 7c8dfef..fe57b5a 100644 --- a/src/components/logos/beatsaver-logo.tsx +++ b/src/components/logos/beatsaver-logo.tsx @@ -3,7 +3,10 @@ type BeatSaverLogoProps = { className?: string; }; -export default function BeatSaverLogo({ size = 32, className }: BeatSaverLogoProps) { +export default function BeatSaverLogo({ + size = 32, + className, +}: BeatSaverLogoProps) { return ( - + diff --git a/src/components/logos/scoresaber-logo.tsx b/src/components/logos/scoresaber-logo.tsx index d5f6802..2fad0d5 100644 --- a/src/components/logos/scoresaber-logo.tsx +++ b/src/components/logos/scoresaber-logo.tsx @@ -2,6 +2,12 @@ import Image from "next/image"; export default function ScoreSaberLogo() { return ( - + ); } diff --git a/src/components/logos/youtube-logo.tsx b/src/components/logos/youtube-logo.tsx index 0576734..384b76f 100644 --- a/src/components/logos/youtube-logo.tsx +++ b/src/components/logos/youtube-logo.tsx @@ -3,7 +3,10 @@ type YouTubeLogoProps = { className?: string; }; -export default function YouTubeLogo({ size = 32, className }: YouTubeLogoProps) { +export default function YouTubeLogo({ + size = 32, + className, +}: YouTubeLogoProps) { return ( - {renderNavbarItem(items[items.length - 1])} + + {renderNavbarItem(items[items.length - 1])} +
diff --git a/src/components/navbar/profile-button.tsx b/src/components/navbar/profile-button.tsx index cd89bd6..c224fa3 100644 --- a/src/components/navbar/profile-button.tsx +++ b/src/components/navbar/profile-button.tsx @@ -20,9 +20,15 @@ export default function ProfileButton() { return ( - + - +

You

diff --git a/src/components/player/player-data.tsx b/src/components/player/player-data.tsx index 375a8f8..43af6fa 100644 --- a/src/components/player/player-data.tsx +++ b/src/components/player/player-data.tsx @@ -18,7 +18,12 @@ type Props = { page: number; }; -export default function PlayerData({ initialPlayerData: initalPlayerData, initialScoreData, sort, page }: Props) { +export default function PlayerData({ + initialPlayerData: initalPlayerData, + initialScoreData, + sort, + page, +}: Props) { let player = initalPlayerData; const { data, isLoading, isError } = useQuery({ queryKey: ["player", player.id], @@ -38,7 +43,12 @@ export default function PlayerData({ initialPlayerData: initalPlayerData, initia )} - + ); } diff --git a/src/components/player/player-header.tsx b/src/components/player/player-header.tsx index 76496c5..0090197 100644 --- a/src/components/player/player-header.tsx +++ b/src/components/player/player-header.tsx @@ -50,13 +50,20 @@ export default function PlayerHeader({ player }: Props) {

{player.name}

- {player.inactive &&

Inactive Account

} - {player.banned &&

Banned Account

} + {player.inactive && ( +

Inactive Account

+ )} + {player.banned && ( +

Banned Account

+ )}
{playerData.map((subName, index) => { // Check if the player is inactive or banned and if the data should be shown - if (!subName.showWhenInactiveOrBanned && (player.inactive || player.banned)) { + if ( + !subName.showWhenInactiveOrBanned && + (player.inactive || player.banned) + ) { return null; } diff --git a/src/components/player/player-rank-chart.tsx b/src/components/player/player-rank-chart.tsx index 7a5f070..fa594a6 100644 --- a/src/components/player/player-rank-chart.tsx +++ b/src/components/player/player-rank-chart.tsx @@ -3,11 +3,28 @@ import ScoreSaberPlayer from "@/common/data-fetcher/types/scoresaber/scoresaber-player"; import { formatNumberWithCommas } from "@/common/number-utils"; -import { CategoryScale, Chart, Legend, LinearScale, LineElement, PointElement, Title, Tooltip } from "chart.js"; +import { + CategoryScale, + Chart, + Legend, + LinearScale, + LineElement, + PointElement, + Title, + Tooltip, +} from "chart.js"; import { Line } from "react-chartjs-2"; import Card from "../card"; -Chart.register(LinearScale, CategoryScale, PointElement, LineElement, Title, Tooltip, Legend); +Chart.register( + LinearScale, + CategoryScale, + PointElement, + LineElement, + Title, + Tooltip, + Legend, +); export const options: any = { maintainAspectRatio: false, diff --git a/src/components/player/player-scores.tsx b/src/components/player/player-scores.tsx index 2adaa96..61e8c88 100644 --- a/src/components/player/player-scores.tsx +++ b/src/components/player/player-scores.tsx @@ -59,14 +59,37 @@ type Props = { page: number; }; -export default function PlayerScores({ initialScoreData, player, sort, page }: Props) { +type PageState = { + /** + * The current page + */ + page: number; + + /** + * The current sort + */ + sort: ScoreSort; +}; + +export default function PlayerScores({ + initialScoreData, + player, + sort, + page, +}: Props) { const { width } = useWindowDimensions(); const controls = useAnimation(); - const [currentSort, setCurrentSort] = useState(sort); + const [firstLoad, setFirstLoad] = useState(true); + const [pageState, setPageState] = useState({ + page: page, + sort: sort, + }); const [previousPage, setPreviousPage] = useState(page); - const [currentPage, setCurrentPage] = useState(page); - const [currentScores, setCurrentScores] = useState(initialScoreData); + const [currentScores, setCurrentScores] = useState< + ScoreSaberPlayerScoresPage | undefined + >(initialScoreData); + const [searchQuery, setSearchQuery] = useState(); const { data: scores, @@ -74,85 +97,114 @@ export default function PlayerScores({ initialScoreData, player, sort, page }: P isLoading, refetch, } = useQuery({ - queryKey: ["playerScores", player.id, currentSort, currentPage], - queryFn: () => scoresaberFetcher.lookupPlayerScores(player.id, currentSort, currentPage), + queryKey: ["playerScores", player.id, pageState], + queryFn: () => + scoresaberFetcher.lookupPlayerScores({ + playerId: player.id, + sort: pageState.sort, + page: pageState.page, + }), staleTime: 30 * 1000, // Cache data for 30 seconds }); const handleScoreLoad = useCallback(async () => { - await controls.start(previousPage >= currentPage ? "hiddenRight" : "hiddenLeft"); + setFirstLoad(false); + if (!firstLoad) { + await controls.start( + previousPage >= pageState.page ? "hiddenRight" : "hiddenLeft", + ); + } setCurrentScores(scores); await controls.start("visible"); - }, [scores, controls]); + }, [scores, controls, previousPage, firstLoad, pageState.page]); + + const handleSortChange = (newSort: ScoreSort) => { + if (newSort !== pageState.sort) { + setPageState({ page: 1, sort: newSort }); + } + }; useEffect(() => { if (scores) { handleScoreLoad(); } - }, [scores]); + }, [scores, isError]); useEffect(() => { - const newUrl = `/player/${player.id}/${currentSort}/${currentPage}`; - window.history.replaceState({ ...window.history.state, as: newUrl, url: newUrl }, "", newUrl); + const newUrl = `/player/${player.id}/${pageState.sort}/${pageState.page}`; + window.history.replaceState( + { ...window.history.state, as: newUrl, url: newUrl }, + "", + newUrl, + ); refetch(); - }, [currentSort, currentPage, refetch, player.id]); - - const handleSortChange = (newSort: ScoreSort) => { - if (newSort !== currentSort) { - setCurrentSort(newSort); - setCurrentPage(1); // Reset page to 1 on sort change - } - }; - - if (currentScores === undefined) { - return undefined; - } + }, [pageState, refetch, player.id]); return ( -
- {Object.values(scoreSort).map((sortOption, index) => ( - + ))} +
+ + {/* todo: add search */} + {/**/} +
+ + {currentScores && ( + <> +
+ {isError &&

Oopsies! Something went wrong.

} + {currentScores.playerScores.length === 0 && ( +

No scores found. Invalid Page?

+ )} +
+ + - {sortOption.icon} - {`${capitalizeFirstLetter(sortOption.name)} Scores`} - - ))} -
- -
- {isError &&

Oopsies! Something went wrong.

} - {currentScores.playerScores.length === 0 &&

No scores found. Invalid Page?

} -
- - - {currentScores.playerScores.map((playerScore, index) => ( - - + {currentScores.playerScores.map((playerScore, index) => ( + + + + ))} - ))} - - { - setPreviousPage(currentPage); - setCurrentPage(page); - }} - /> + { + setPreviousPage(pageState.page); + setPageState({ page, sort: pageState.sort }); + }} + /> + + )} ); } diff --git a/src/components/player/player-stats.tsx b/src/components/player/player-stats.tsx index 24b550c..cc8aaac 100644 --- a/src/components/player/player-stats.tsx +++ b/src/components/player/player-stats.tsx @@ -57,14 +57,23 @@ type Props = { export default function PlayerStats({ player }: Props) { return ( -
+
{badges.map((badge, index) => { const toRender = badge.create(player); if (toRender === undefined) { return
; } - return ; + return ( + + ); })}
); diff --git a/src/components/player/score/leaderboard-button.tsx b/src/components/player/score/leaderboard-button.tsx index f27d884..00af485 100644 --- a/src/components/player/score/leaderboard-button.tsx +++ b/src/components/player/score/leaderboard-button.tsx @@ -8,7 +8,10 @@ type Props = { setIsLeaderboardExpanded: Dispatch>; }; -export default function LeaderboardButton({ isLeaderboardExpanded, setIsLeaderboardExpanded }: Props) { +export default function LeaderboardButton({ + isLeaderboardExpanded, + setIsLeaderboardExpanded, +}: Props) { return (
diff --git a/src/components/player/score/score-buttons.tsx b/src/components/player/score/score-buttons.tsx index 54b955f..e2e150b 100644 --- a/src/components/player/score/score-buttons.tsx +++ b/src/components/player/score/score-buttons.tsx @@ -53,7 +53,10 @@ export default function ScoreButtons({ {/* Open map in BeatSaver */} { - window.open(`https://beatsaver.com/maps/${beatSaverMap.bsr}`, "_blank"); + window.open( + `https://beatsaver.com/maps/${beatSaverMap.bsr}`, + "_blank", + ); }} tooltip={

Click to open the map

} > @@ -66,8 +69,12 @@ export default function ScoreButtons({ { window.open( - songNameToYouTubeLink(leaderboard.songName, leaderboard.songSubName, leaderboard.songAuthorName), - "_blank" + songNameToYouTubeLink( + leaderboard.songName, + leaderboard.songSubName, + leaderboard.songAuthorName, + ), + "_blank", ); }} tooltip={

Click to open the song in YouTube

} diff --git a/src/components/player/score/score-info.tsx b/src/components/player/score/score-info.tsx index 9ee1dc1..469bad8 100644 --- a/src/components/player/score/score-info.tsx +++ b/src/components/player/score/score-info.tsx @@ -14,9 +14,13 @@ type Props = { }; export default function ScoreSongInfo({ leaderboard, beatSaverMap }: Props) { - const diff = getDifficultyFromScoreSaberDifficulty(leaderboard.difficulty.difficulty); + const diff = getDifficultyFromScoreSaberDifficulty( + leaderboard.difficulty.difficulty, + ); const mappersProfile = - beatSaverMap != undefined ? `https://beatsaver.com/profile/${beatSaverMap?.fullData.uploader.id}` : undefined; + beatSaverMap != undefined + ? `https://beatsaver.com/profile/${beatSaverMap?.fullData.uploader.id}` + : undefined; return (
@@ -68,7 +72,13 @@ export default function ScoreSongInfo({ leaderboard, beatSaverMap }: Props) {

{leaderboard.songAuthorName}

-

+

{leaderboard.levelAuthorName}

diff --git a/src/components/player/score/score-stats.tsx b/src/components/player/score/score-stats.tsx index abdef81..f44645d 100644 --- a/src/components/player/score/score-stats.tsx +++ b/src/components/player/score/score-stats.tsx @@ -7,7 +7,10 @@ import clsx from "clsx"; type Badge = { name: string; - create: (score: ScoreSaberScore, leaderboard: ScoreSaberLeaderboard) => string | React.ReactNode | undefined; + create: ( + score: ScoreSaberScore, + leaderboard: ScoreSaberLeaderboard, + ) => string | React.ReactNode | undefined; }; const badges: Badge[] = [ @@ -49,8 +52,16 @@ const badges: Badge[] = [ return ( <> -

{fullCombo ? FC : formatNumberWithCommas(score.missedNotes)}

- +

+ {fullCombo ? ( + FC + ) : ( + formatNumberWithCommas(score.missedNotes) + )} +

+ ); }, diff --git a/src/components/providers/query-provider.tsx b/src/components/providers/query-provider.tsx index c19f40e..d45b729 100644 --- a/src/components/providers/query-provider.tsx +++ b/src/components/providers/query-provider.tsx @@ -9,5 +9,7 @@ type Props = { const queryClient = new QueryClient(); export function QueryProvider({ children }: Props) { - return {children}; + return ( + {children} + ); } diff --git a/src/components/stat-value.tsx b/src/components/stat-value.tsx index cafb08b..813edbc 100644 --- a/src/components/stat-value.tsx +++ b/src/components/stat-value.tsx @@ -22,7 +22,7 @@ export default function StatValue({ name, color, value }: Props) {
{name && ( @@ -31,7 +31,9 @@ export default function StatValue({ name, color, value }: Props) {
)} -
{typeof value === "string" ?

{value}

: value}
+
+ {typeof value === "string" ?

{value}

: value} +
); } diff --git a/src/components/tooltip.tsx b/src/components/tooltip.tsx index 00913a5..a14a4dd 100644 --- a/src/components/tooltip.tsx +++ b/src/components/tooltip.tsx @@ -1,4 +1,8 @@ -import { Tooltip as ShadCnTooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip"; +import { + Tooltip as ShadCnTooltip, + TooltipContent, + TooltipTrigger, +} from "./ui/tooltip"; type Props = { /** diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index 924040c..bd2c9f0 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -11,7 +11,10 @@ const Avatar = React.forwardRef< >(({ className, ...props }, ref) => ( )); @@ -21,7 +24,11 @@ const AvatarImage = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); AvatarImage.displayName = AvatarPrimitive.Image.displayName; @@ -31,7 +38,10 @@ const AvatarFallback = React.forwardRef< >(({ className, ...props }, ref) => ( )); diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index c4129f8..0026f98 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -9,10 +9,14 @@ const buttonVariants = cva( { variants: { variant: { - default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", - destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", - outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", - secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, @@ -27,7 +31,7 @@ const buttonVariants = cva( variant: "default", size: "default", }, - } + }, ); export interface ButtonProps @@ -39,8 +43,14 @@ export interface ButtonProps const Button = React.forwardRef( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button"; - return ; - } + return ( + + ); + }, ); Button.displayName = "Button"; diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx index 257a7c9..0505d9e 100644 --- a/src/components/ui/card.tsx +++ b/src/components/ui/card.tsx @@ -2,40 +2,82 @@ import * as React from "react"; import { cn } from "@/common/utils"; -const Card = React.forwardRef>(({ className, ...props }, ref) => ( -
+const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
)); Card.displayName = "Card"; -const CardHeader = React.forwardRef>( - ({ className, ...props }, ref) => ( -
- ) -); +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); CardHeader.displayName = "CardHeader"; -const CardTitle = React.forwardRef>( - ({ className, ...props }, ref) => ( -

- ) -); +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); CardTitle.displayName = "CardTitle"; -const CardDescription = React.forwardRef>( - ({ className, ...props }, ref) => ( -

- ) -); +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)); CardDescription.displayName = "CardDescription"; -const CardContent = React.forwardRef>( - ({ className, ...props }, ref) =>

-); +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); CardContent.displayName = "CardContent"; -const CardFooter = React.forwardRef>( - ({ className, ...props }, ref) =>
-); +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); CardFooter.displayName = "CardFooter"; -export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }; +export { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +}; diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx index 684108a..bcb263a 100644 --- a/src/components/ui/form.tsx +++ b/src/components/ui/form.tsx @@ -3,7 +3,14 @@ import * as React from "react"; import * as LabelPrimitive from "@radix-ui/react-label"; import { Slot } from "@radix-ui/react-slot"; -import { Controller, ControllerProps, FieldPath, FieldValues, FormProvider, useFormContext } from "react-hook-form"; +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form"; import { cn } from "@/common/utils"; import { Label } from "@/components/ui/label"; @@ -12,16 +19,18 @@ const Form = FormProvider; type FormFieldContextValue< TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, > = { name: TName; }; -const FormFieldContext = React.createContext({} as FormFieldContextValue); +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); const FormField = < TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TName extends FieldPath = FieldPath, >({ ...props }: ControllerProps) => { @@ -59,19 +68,22 @@ type FormItemContextValue = { id: string; }; -const FormItemContext = React.createContext({} as FormItemContextValue); - -const FormItem = React.forwardRef>( - ({ className, ...props }, ref) => { - const id = React.useId(); - - return ( - -
- - ); - } +const FormItemContext = React.createContext( + {} as FormItemContextValue, ); + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); +}); FormItem.displayName = "FormItem"; const FormLabel = React.forwardRef< @@ -80,59 +92,88 @@ const FormLabel = React.forwardRef< >(({ className, ...props }, ref) => { const { error, formItemId } = useFormField(); - return