Compare commits
2 Commits
d81dac3622
...
773fe0740a
Author | SHA1 | Date | |
---|---|---|---|
773fe0740a | |||
76f1422bd7 |
@ -23,12 +23,14 @@
|
|||||||
"@trigger.dev/nextjs": "^3.0.8",
|
"@trigger.dev/nextjs": "^3.0.8",
|
||||||
"@trigger.dev/react": "^3.0.8",
|
"@trigger.dev/react": "^3.0.8",
|
||||||
"@trigger.dev/sdk": "^3.0.8",
|
"@trigger.dev/sdk": "^3.0.8",
|
||||||
|
"canvas": "3.0.0-rc2",
|
||||||
"chart.js": "^4.4.4",
|
"chart.js": "^4.4.4",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"comlink": "^4.4.1",
|
"comlink": "^4.4.1",
|
||||||
"dexie": "^4.0.8",
|
"dexie": "^4.0.8",
|
||||||
"dexie-react-hooks": "^1.1.7",
|
"dexie-react-hooks": "^1.1.7",
|
||||||
|
"extract-colors": "^4.0.8",
|
||||||
"framer-motion": "^11.5.4",
|
"framer-motion": "^11.5.4",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
"ky": "^1.7.2",
|
"ky": "^1.7.2",
|
||||||
|
@ -3,8 +3,16 @@ import { scoresaberService } from "@/common/service/impl/scoresaber";
|
|||||||
import { ScoreSort } from "@/common/service/score-sort";
|
import { ScoreSort } from "@/common/service/score-sort";
|
||||||
import PlayerData from "@/components/player/player-data";
|
import PlayerData from "@/components/player/player-data";
|
||||||
import { format } from "@formkit/tempo";
|
import { format } from "@formkit/tempo";
|
||||||
import { Metadata } from "next";
|
import { Metadata, Viewport } from "next";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { Colors } from "@/common/colors";
|
||||||
|
import ScoreSaberPlayerScoresPageToken from "@/common/model/token/scoresaber/score-saber-player-scores-page-token";
|
||||||
|
import { getAverageColor } from "@/common/image-utils";
|
||||||
|
|
||||||
|
const UNKNOWN_PLAYER = {
|
||||||
|
title: "ScoreSaber Reloaded - Unknown Player",
|
||||||
|
description: "The player you were looking for could not be found.",
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: Promise<{
|
params: Promise<{
|
||||||
@ -15,19 +23,52 @@ type Props = {
|
|||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
/**
|
||||||
|
* Gets the player data and scores
|
||||||
|
*
|
||||||
|
* @param params the params
|
||||||
|
* @param fetchScores whether to fetch the scores
|
||||||
|
* @returns the player data and scores
|
||||||
|
*/
|
||||||
|
async function getPlayerData({ params }: Props, fetchScores: boolean = true) {
|
||||||
const { slug } = await params;
|
const { slug } = await params;
|
||||||
const id = slug[0]; // The players id
|
const id = slug[0]; // The players id
|
||||||
const response = await scoresaberService.lookupPlayer(id, false);
|
const sort: ScoreSort = (slug[1] as ScoreSort) || "recent"; // The sorting method
|
||||||
if (response === undefined) {
|
const page = parseInt(slug[2]) || 1; // The page number
|
||||||
|
const search = (slug[3] as string) || ""; // The search query
|
||||||
|
|
||||||
|
const player = (await scoresaberService.lookupPlayer(id, false))?.player;
|
||||||
|
let scores: ScoreSaberPlayerScoresPageToken | undefined;
|
||||||
|
if (fetchScores) {
|
||||||
|
scores = await scoresaberService.lookupPlayerScores({
|
||||||
|
playerId: id,
|
||||||
|
sort,
|
||||||
|
page,
|
||||||
|
search,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sort: sort,
|
||||||
|
page: page,
|
||||||
|
search: search,
|
||||||
|
player: player,
|
||||||
|
scores: scores,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata(props: Props): Promise<Metadata> {
|
||||||
|
const { player } = await getPlayerData(props, false);
|
||||||
|
if (player === undefined) {
|
||||||
return {
|
return {
|
||||||
title: `Unknown Player`,
|
title: UNKNOWN_PLAYER.title,
|
||||||
|
description: UNKNOWN_PLAYER.description,
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `Unknown Player`,
|
title: UNKNOWN_PLAYER.title,
|
||||||
|
description: UNKNOWN_PLAYER.description,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const { player } = response;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `${player.name}`,
|
title: `${player.name}`,
|
||||||
@ -43,27 +84,32 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function Search({ params, searchParams }: Props) {
|
export async function generateViewport(props: Props): Promise<Viewport> {
|
||||||
const { slug } = await params;
|
const { player } = await getPlayerData(props, false);
|
||||||
const searchParamss = await searchParams;
|
if (player === undefined) {
|
||||||
const id = slug[0]; // The players id
|
return {
|
||||||
const sort: ScoreSort = (slug[1] as ScoreSort) || "recent"; // The sorting method
|
themeColor: Colors.primary,
|
||||||
const page = parseInt(slug[2]) || 1; // The page number
|
};
|
||||||
const search = searchParamss["search"] || ""; // The search query
|
}
|
||||||
|
|
||||||
const response = await scoresaberService.lookupPlayer(id, false);
|
const color = await getAverageColor(player.avatar);
|
||||||
if (response == undefined) {
|
if (color === undefined) {
|
||||||
// Invalid player id
|
return {
|
||||||
|
themeColor: Colors.primary,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
themeColor: color?.hex,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Search(props: Props) {
|
||||||
|
const { player, scores, sort, page, search } = await getPlayerData(props);
|
||||||
|
if (player == undefined) {
|
||||||
return redirect("/");
|
return redirect("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
const scores = await scoresaberService.lookupPlayerScores({
|
|
||||||
playerId: id,
|
|
||||||
sort,
|
|
||||||
page,
|
|
||||||
search,
|
|
||||||
});
|
|
||||||
const { player } = response;
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full w-full">
|
<div className="flex flex-col h-full w-full">
|
||||||
<PlayerData
|
<PlayerData
|
||||||
|
@ -5,13 +5,14 @@ import { ThemeProvider } from "@/components/providers/theme-provider";
|
|||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { AnimatePresence } from "framer-motion";
|
import { AnimatePresence } from "framer-motion";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata, Viewport } from "next";
|
||||||
import localFont from "next/font/local";
|
import localFont from "next/font/local";
|
||||||
import BackgroundImage from "../components/background-image";
|
import BackgroundImage from "../components/background-image";
|
||||||
import DatabaseLoader from "../components/loaders/database-loader";
|
import DatabaseLoader from "../components/loaders/database-loader";
|
||||||
import NavBar from "../components/navbar/navbar";
|
import NavBar from "../components/navbar/navbar";
|
||||||
|
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import { Colors } from "@/common/colors";
|
||||||
|
|
||||||
const siteFont = localFont({
|
const siteFont = localFont({
|
||||||
src: "./fonts/JetBrainsMono-Regular.woff2",
|
src: "./fonts/JetBrainsMono-Regular.woff2",
|
||||||
@ -57,6 +58,10 @@ export const metadata: Metadata = {
|
|||||||
"Scoresaber Reloaded is a new way to view your scores and get more stats about your and your plays",
|
"Scoresaber Reloaded is a new way to view your scores and get more stats about your and your plays",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const viewport: Viewport = {
|
||||||
|
themeColor: Colors.primary,
|
||||||
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
|
3
src/common/colors.ts
Normal file
3
src/common/colors.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const Colors = {
|
||||||
|
primary: "#0070f3",
|
||||||
|
};
|
@ -1,4 +1,8 @@
|
|||||||
|
import { createCanvas, loadImage } from "canvas";
|
||||||
import { config } from "../../config";
|
import { config } from "../../config";
|
||||||
|
import ky from "ky";
|
||||||
|
import { extractColors } from "extract-colors";
|
||||||
|
import { cache } from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Proxies all non-localhost images to make them load faster.
|
* Proxies all non-localhost images to make them load faster.
|
||||||
@ -9,3 +13,45 @@ import { config } from "../../config";
|
|||||||
export function getImageUrl(originalUrl: string) {
|
export function getImageUrl(originalUrl: string) {
|
||||||
return `${!config.siteUrl.includes("localhost") ? "https://img.fascinated.cc/upload/q_70/" : ""}${originalUrl}`;
|
return `${!config.siteUrl.includes("localhost") ? "https://img.fascinated.cc/upload/q_70/" : ""}${originalUrl}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the average color of an image
|
||||||
|
*
|
||||||
|
* @param src the image url
|
||||||
|
* @returns the average color
|
||||||
|
*/
|
||||||
|
export const getAverageColor = cache(async (src: string) => {
|
||||||
|
const before = performance.now();
|
||||||
|
console.log(`Getting average color of "${src}"...`);
|
||||||
|
try {
|
||||||
|
const response = await ky.get(src);
|
||||||
|
if (response.status !== 200) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageBuffer = await response.arrayBuffer();
|
||||||
|
|
||||||
|
// Create an image from the buffer using canvas
|
||||||
|
const img = await loadImage(Buffer.from(imageBuffer));
|
||||||
|
const canvas = createCanvas(img.width, img.height);
|
||||||
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
|
// Draw the image onto the canvas
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
// Get the pixel data from the canvas
|
||||||
|
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
||||||
|
const { data, width, height } = imageData;
|
||||||
|
|
||||||
|
// Use your extractColors function to calculate the average color
|
||||||
|
const color = await extractColors({ data, width, height });
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Found average color of "${src}" in ${(performance.now() - before).toFixed(0)}ms`,
|
||||||
|
);
|
||||||
|
return color[2];
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error while getting average color:", error);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -121,8 +121,9 @@ export async function getScoreSaberPlayerFromToken(
|
|||||||
}
|
}
|
||||||
// Sort the fallback history
|
// Sort the fallback history
|
||||||
statisticHistory = Object.entries(statisticHistory)
|
statisticHistory = Object.entries(statisticHistory)
|
||||||
.sort()
|
.sort((a, b) => Date.parse(b[0]) - Date.parse(a[0]))
|
||||||
.reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {});
|
.reduce((obj, [key, value]) => ({ ...obj, [key]: value }), {});
|
||||||
|
|
||||||
const yesterdayDate = formatDateMinimal(
|
const yesterdayDate = formatDateMinimal(
|
||||||
getMidnightAlignedDate(getDaysAgoDate(1)),
|
getMidnightAlignedDate(getDaysAgoDate(1)),
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user