2 Commits

Author SHA1 Message Date
773fe0740a Merge remote-tracking branch 'origin/master'
Some checks failed
Deploy / deploy (push) Failing after 24s
# Conflicts:
#	pnpm-lock.yaml
2024-09-30 08:35:11 +01:00
76f1422bd7 update player page embed to so the embed color is the avg color of their avatar and fixed history sorting 2024-09-30 08:35:00 +01:00
6 changed files with 129 additions and 26 deletions

View File

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

View File

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

View File

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

@ -0,0 +1,3 @@
export const Colors = {
primary: "#0070f3",
};

View File

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

View File

@ -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)),
); );