add cool og image for player embed
This commit is contained in:
parent
005e05d8fb
commit
ef634194b8
@ -123,6 +123,7 @@ export class PlayerService {
|
|||||||
*/
|
*/
|
||||||
public static async trackScore({ score, leaderboard }: ScoreSaberPlayerScoreToken) {
|
public static async trackScore({ score, leaderboard }: ScoreSaberPlayerScoreToken) {
|
||||||
const playerId = score.leaderboardPlayerInfo.id;
|
const playerId = score.leaderboardPlayerInfo.id;
|
||||||
|
const playerName = score.leaderboardPlayerInfo.name;
|
||||||
const player: PlayerDocument | null = await PlayerModel.findById(playerId);
|
const player: PlayerDocument | null = await PlayerModel.findById(playerId);
|
||||||
// Player is not tracked, so ignore the score.
|
// Player is not tracked, so ignore the score.
|
||||||
if (player == undefined) {
|
if (player == undefined) {
|
||||||
@ -151,7 +152,7 @@ export class PlayerService {
|
|||||||
await player.save();
|
await player.save();
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`Updated scores set statistic for "${player.id}", scores today: ${scores.rankedScores} ranked, ${scores.unrankedScores} unranked`
|
`Updated scores set statistic for "${playerName}"(${playerId}), scores today: ${scores.rankedScores} ranked, ${scores.unrankedScores} unranked`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
58
projects/website/src/app/(pages)/api/og/player/route.tsx
Normal file
58
projects/website/src/app/(pages)/api/og/player/route.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { ImageResponse } from "next/og";
|
||||||
|
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
import { formatNumberWithCommas, formatPp } from "@/common/number-utils";
|
||||||
|
import { config } from "../../../../../../config";
|
||||||
|
|
||||||
|
export async function GET(request: NextRequest) {
|
||||||
|
const playerId = request.nextUrl.searchParams.get("id");
|
||||||
|
if (!playerId) {
|
||||||
|
return new Response(null, { status: 400 });
|
||||||
|
}
|
||||||
|
const player = await scoresaberService.lookupPlayer(playerId);
|
||||||
|
if (!player) {
|
||||||
|
return new Response(null, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ImageResponse(
|
||||||
|
(
|
||||||
|
<div tw="w-full h-full flex flex-col text-white bg-black text-3xl p-3 justify-center items-center">
|
||||||
|
<img src={player.profilePicture} width={256} height={256} alt="Player's Avatar" tw="rounded-full" />
|
||||||
|
<div tw="flex flex-col pl-3 items-center">
|
||||||
|
<p tw="font-bold text-6xl m-0">{player.name}</p>
|
||||||
|
<p tw="text-[#606fff] m-0">{formatPp(player.pp)}pp</p>
|
||||||
|
<div tw="flex">
|
||||||
|
<div tw="flex px-2 justify-center items-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 26"
|
||||||
|
fill="currentColor"
|
||||||
|
style={{
|
||||||
|
width: "33px",
|
||||||
|
height: "33px",
|
||||||
|
paddingRight: "3px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25ZM6.262 6.072a8.25 8.25 0 1 0 10.562-.766 4.5 4.5 0 0 1-1.318 1.357L14.25 7.5l.165.33a.809.809 0 0 1-1.086 1.085l-.604-.302a1.125 1.125 0 0 0-1.298.21l-.132.131c-.439.44-.439 1.152 0 1.591l.296.296c.256.257.622.374.98.314l1.17-.195c.323-.054.654.036.905.245l1.33 1.108c.32.267.46.694.358 1.1a8.7 8.7 0 0 1-2.288 4.04l-.723.724a1.125 1.125 0 0 1-1.298.21l-.153-.076a1.125 1.125 0 0 1-.622-1.006v-1.089c0-.298-.119-.585-.33-.796l-1.347-1.347a1.125 1.125 0 0 1-.21-1.298L9.75 12l-1.64-1.64a6 6 0 0 1-1.676-3.257l-.172-1.03Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<p tw="m-0">#{formatNumberWithCommas(player.rank)}</p>
|
||||||
|
</div>
|
||||||
|
<div tw="flex items-center px-2 justify-center items-center">
|
||||||
|
<img src={`${config.siteUrl}/assets/flags/${player.country}.png`} height={20} alt="Player's Country" />
|
||||||
|
<p tw="pl-1 m-0">#{formatNumberWithCommas(player.countryRank)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
emoji: "twemoji",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,4 @@
|
|||||||
import { formatNumberWithCommas, formatPp } from "@/common/number-utils";
|
|
||||||
import PlayerData from "@/components/player/player-data";
|
import PlayerData from "@/components/player/player-data";
|
||||||
import { format } from "@formkit/tempo";
|
|
||||||
import { Metadata, Viewport } from "next";
|
import { Metadata, Viewport } from "next";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { Colors } from "@/common/colors";
|
import { Colors } from "@/common/colors";
|
||||||
@ -97,20 +95,20 @@ export async function generateMetadata(props: Props): Promise<Metadata> {
|
|||||||
title: `${player.name}`,
|
title: `${player.name}`,
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `ScoreSaber Reloaded - ${player.name}`,
|
title: `ScoreSaber Reloaded - ${player.name}`,
|
||||||
description: `
|
// description: `
|
||||||
PP: ${formatPp(player.pp)}pp
|
// PP: ${formatPp(player.pp)}pp
|
||||||
Rank: #${formatNumberWithCommas(player.rank)} (#${formatNumberWithCommas(player.countryRank)} ${player.country})
|
// Rank: #${formatNumberWithCommas(player.rank)} (#${formatNumberWithCommas(player.countryRank)} ${player.country})
|
||||||
Joined ScoreSaber: ${format(player.joinedDate, { date: "medium", time: "short" })}
|
// Joined ScoreSaber: ${format(player.joinedDate, { date: "medium", time: "short" })}
|
||||||
|
//
|
||||||
View the scores for ${player.name}!`,
|
// View the scores for ${player.name}!`,
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: player.avatar,
|
url: `${config.siteUrl}/api/og/player/?id=${player.id}`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: "summary",
|
card: "summary_large_image",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -2,25 +2,22 @@
|
|||||||
|
|
||||||
import { GlobeAmericasIcon } from "@heroicons/react/24/solid";
|
import { GlobeAmericasIcon } from "@heroicons/react/24/solid";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Card from "@/components/card";
|
|
||||||
|
|
||||||
export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
export default function Error({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<div className="flex flex-col items-center justify-center text-center">
|
||||||
<div className="flex flex-col items-center justify-center text-center h-screen">
|
<GlobeAmericasIcon className="h-24 w-24 text-red-500" />
|
||||||
<GlobeAmericasIcon className="h-24 w-24 text-red-500" />
|
<h1 className="text-4xl font-bold text-gray-200 mt-6">Oops! Something went wrong.</h1>
|
||||||
<h1 className="text-4xl font-bold text-gray-200 mt-6">Oops! Something went wrong.</h1>
|
<p className="text-lg text-gray-400 mt-2">
|
||||||
<p className="text-lg text-gray-400 mt-2">
|
We're experiencing some technical difficulties. Please try again later.
|
||||||
We're experiencing some technical difficulties. Please try again later.
|
</p>
|
||||||
</p>
|
{error?.digest && <p className="text-sm text-gray-500 mt-1">Error Code: {error.digest}</p>}
|
||||||
{error?.digest && <p className="text-sm text-gray-500 mt-1">Error Code: {error.digest}</p>}
|
|
||||||
|
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<Link href="/" className="text-blue-500 hover:underline">
|
<Link href="/" className="text-blue-500 hover:underline">
|
||||||
Go back to the homepage
|
Go back to the homepage
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Chart, registerables } from "chart.js";
|
import { Chart, registerables } from "chart.js";
|
||||||
Chart.register(...registerables);
|
|
||||||
|
|
||||||
import { Line } from "react-chartjs-2";
|
import { Line } from "react-chartjs-2";
|
||||||
import { useIsMobile } from "@/hooks/use-is-mobile";
|
import { useIsMobile } from "@/hooks/use-is-mobile";
|
||||||
import { formatDateMinimal, getDaysAgo, getDaysAgoDate, parseDate } from "@ssr/common/utils/time-utils";
|
import { formatDateMinimal, getDaysAgo, getDaysAgoDate, parseDate } from "@ssr/common/utils/time-utils";
|
||||||
|
|
||||||
|
Chart.register(...registerables);
|
||||||
|
|
||||||
export type AxisPosition = "left" | "right";
|
export type AxisPosition = "left" | "right";
|
||||||
export type DatasetDisplayType = "line" | "bar";
|
export type DatasetDisplayType = "line" | "bar";
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ArrowPathIcon } from "@heroicons/react/24/solid";
|
import { ArrowPathIcon } from "@heroicons/react/24/solid";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import * as React from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Pagination as ShadCnPagination,
|
Pagination as ShadCnPagination,
|
||||||
@ -10,7 +11,6 @@ import {
|
|||||||
PaginationNext,
|
PaginationNext,
|
||||||
PaginationPrevious,
|
PaginationPrevious,
|
||||||
} from "../ui/pagination";
|
} from "../ui/pagination";
|
||||||
import * as React from "react";
|
|
||||||
import { ChevronDoubleLeftIcon, ChevronDoubleRightIcon } from "@heroicons/react/16/solid";
|
import { ChevronDoubleLeftIcon, ChevronDoubleRightIcon } from "@heroicons/react/16/solid";
|
||||||
|
|
||||||
type PaginationItemWrapperProps = {
|
type PaginationItemWrapperProps = {
|
||||||
|
Reference in New Issue
Block a user