This commit is contained in:
parent
ee1c33bcc9
commit
78c88acddf
20
projects/backend/components/globe-icon.tsx
Normal file
20
projects/backend/components/globe-icon.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const GlobeIcon = () => (
|
||||||
|
<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>
|
||||||
|
);
|
20
projects/backend/components/star-icon.tsx
Normal file
20
projects/backend/components/star-icon.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const StarIcon = () => (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
style={{
|
||||||
|
width: "40px",
|
||||||
|
height: "40px",
|
||||||
|
paddingRight: "3px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
@ -13,7 +13,6 @@
|
|||||||
"@elysiajs/swagger": "^1.1.3",
|
"@elysiajs/swagger": "^1.1.3",
|
||||||
"@ssr/common": "workspace:common",
|
"@ssr/common": "workspace:common",
|
||||||
"@tqman/nice-logger": "^1.0.1",
|
"@tqman/nice-logger": "^1.0.1",
|
||||||
"@typegoose/typegoose": "^12.8.0",
|
|
||||||
"@vercel/og": "^0.6.3",
|
"@vercel/og": "^0.6.3",
|
||||||
"elysia": "latest",
|
"elysia": "latest",
|
||||||
"elysia-autoroutes": "^0.5.0",
|
"elysia-autoroutes": "^0.5.0",
|
||||||
|
@ -2,15 +2,39 @@ import { ImageResponse } from "@vercel/og";
|
|||||||
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
|
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { formatNumberWithCommas, formatPp } from "@ssr/common/utils/number-utils";
|
import { formatNumberWithCommas, formatPp } from "@ssr/common/utils/number-utils";
|
||||||
import NodeCache from "node-cache";
|
|
||||||
import { getDifficultyFromScoreSaberDifficulty } from "@ssr/common/utils/scoresaber-utils";
|
import { getDifficultyFromScoreSaberDifficulty } from "@ssr/common/utils/scoresaber-utils";
|
||||||
|
import { StarIcon } from "../../components/star-icon";
|
||||||
|
import { GlobeIcon } from "../../components/globe-icon";
|
||||||
|
import NodeCache from "node-cache";
|
||||||
|
import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token";
|
||||||
|
import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token";
|
||||||
|
|
||||||
const imageCache = new NodeCache({
|
const cache = new NodeCache({
|
||||||
stdTTL: 60 * 60, // 1 hour
|
stdTTL: 60 * 60, // 1 hour
|
||||||
checkperiod: 120,
|
checkperiod: 120,
|
||||||
});
|
});
|
||||||
|
|
||||||
export class ImageService {
|
export class ImageService {
|
||||||
|
/**
|
||||||
|
* The base of the OpenGraph image
|
||||||
|
*
|
||||||
|
* @param children the content of the image
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
public static BaseImage({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
tw="w-full h-full flex flex-col text-white text-3xl p-3 justify-center items-center"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#0a0a0a",
|
||||||
|
background: "radial-gradient(ellipse 60% 60% at 50% -20%, rgba(120,119,198,0.15), rgba(255,255,255,0))",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the OpenGraph image for the player
|
* Generates the OpenGraph image for the player
|
||||||
*
|
*
|
||||||
@ -18,45 +42,29 @@ export class ImageService {
|
|||||||
*/
|
*/
|
||||||
public static async generatePlayerImage(id: string) {
|
public static async generatePlayerImage(id: string) {
|
||||||
const cacheKey = `player-${id}`;
|
const cacheKey = `player-${id}`;
|
||||||
if (imageCache.has(cacheKey)) {
|
let player: undefined | ScoreSaberPlayerToken;
|
||||||
return imageCache.get(cacheKey) as ImageResponse;
|
if (cache.has(cacheKey)) {
|
||||||
|
player = cache.get<ScoreSaberPlayerToken>(cacheKey);
|
||||||
|
} else {
|
||||||
|
player = await scoresaberService.lookupPlayer(id);
|
||||||
|
if (player != undefined) {
|
||||||
|
cache.set(cacheKey, player);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = await scoresaberService.lookupPlayer(id);
|
|
||||||
if (player == undefined) {
|
if (player == undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const imageResponse = new ImageResponse(
|
|
||||||
|
return new ImageResponse(
|
||||||
(
|
(
|
||||||
<div
|
<ImageService.BaseImage>
|
||||||
tw="w-full h-full flex flex-col text-white text-3xl p-3 justify-center items-center"
|
|
||||||
style={{
|
|
||||||
backgroundColor: "#0a0a0a",
|
|
||||||
background: "radial-gradient(ellipse 60% 60% at 50% -20%, rgba(120,119,198,0.15), rgba(255,255,255,0))",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img src={player.profilePicture} width={256} height={256} alt="Player's Avatar" tw="rounded-full mb-3" />
|
<img src={player.profilePicture} width={256} height={256} alt="Player's Avatar" tw="rounded-full mb-3" />
|
||||||
<div tw="flex flex-col pl-3 items-center">
|
<div tw="flex flex-col pl-3 items-center">
|
||||||
<p tw="font-bold text-6xl m-0">{player.name}</p>
|
<p tw="font-bold text-6xl m-0">{player.name}</p>
|
||||||
<p tw="text-[#606fff] m-0">{formatPp(player.pp)}pp</p>
|
<p tw="text-[#606fff] m-0">{formatPp(player.pp)}pp</p>
|
||||||
<div tw="flex">
|
<div tw="flex">
|
||||||
<div tw="flex px-2 justify-center items-center">
|
<div tw="flex px-2 justify-center items-center">
|
||||||
<svg
|
<GlobeIcon />
|
||||||
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>
|
<p tw="m-0">#{formatNumberWithCommas(player.rank)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div tw="flex items-center px-2 justify-center items-center">
|
<div tw="flex items-center px-2 justify-center items-center">
|
||||||
@ -70,7 +78,7 @@ export class ImageService {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ImageService.BaseImage>
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
width: 1200,
|
width: 1200,
|
||||||
@ -78,8 +86,6 @@ export class ImageService {
|
|||||||
emoji: "twemoji",
|
emoji: "twemoji",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
imageCache.set(cacheKey, imageResponse);
|
|
||||||
return imageResponse;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -89,25 +95,23 @@ export class ImageService {
|
|||||||
*/
|
*/
|
||||||
public static async generateLeaderboardImage(id: string) {
|
public static async generateLeaderboardImage(id: string) {
|
||||||
const cacheKey = `leaderboard-${id}`;
|
const cacheKey = `leaderboard-${id}`;
|
||||||
if (imageCache.has(cacheKey)) {
|
let leaderboard: undefined | ScoreSaberLeaderboardToken;
|
||||||
return imageCache.get(cacheKey) as ImageResponse;
|
if (cache.has(cacheKey)) {
|
||||||
|
leaderboard = cache.get(cacheKey) as ScoreSaberLeaderboardToken;
|
||||||
|
} else {
|
||||||
|
leaderboard = await scoresaberService.lookupLeaderboard(id);
|
||||||
|
if (leaderboard != undefined) {
|
||||||
|
cache.set(cacheKey, leaderboard);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const leaderboard = await scoresaberService.lookupLeaderboard(id);
|
|
||||||
if (leaderboard == undefined) {
|
if (leaderboard == undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ranked = leaderboard.stars > 0;
|
const ranked = leaderboard.stars > 0;
|
||||||
const imageResponse = new ImageResponse(
|
return new ImageResponse(
|
||||||
(
|
(
|
||||||
<div
|
<ImageService.BaseImage>
|
||||||
tw="w-full h-full flex flex-col text-white text-3xl p-3 justify-center items-center"
|
|
||||||
style={{
|
|
||||||
backgroundColor: "#0a0a0a",
|
|
||||||
background: "radial-gradient(ellipse 60% 60% at 50% -20%, rgba(120,119,198,0.15), rgba(255,255,255,0))",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img src={leaderboard.coverImage} width={256} height={256} alt="Player's Avatar" tw="rounded-full mb-3" />
|
<img src={leaderboard.coverImage} width={256} height={256} alt="Player's Avatar" tw="rounded-full mb-3" />
|
||||||
<p tw="font-bold text-6xl m-0">
|
<p tw="font-bold text-6xl m-0">
|
||||||
{leaderboard.songName} {leaderboard.songSubName}
|
{leaderboard.songName} {leaderboard.songSubName}
|
||||||
@ -116,22 +120,7 @@ export class ImageService {
|
|||||||
{ranked && (
|
{ranked && (
|
||||||
<div tw="flex justify-center items-center text-4xl">
|
<div tw="flex justify-center items-center text-4xl">
|
||||||
<p tw="font-bold m-0">{leaderboard.stars}</p>
|
<p tw="font-bold m-0">{leaderboard.stars}</p>
|
||||||
<svg
|
<StarIcon />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
style={{
|
|
||||||
width: "40px",
|
|
||||||
height: "40px",
|
|
||||||
paddingRight: "3px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.006 5.404.434c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.434 2.082-5.005Z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<p tw={"font-bold m-0 text-4xl" + (ranked ? " pl-3" : "")}>
|
<p tw={"font-bold m-0 text-4xl" + (ranked ? " pl-3" : "")}>
|
||||||
@ -139,7 +128,7 @@ export class ImageService {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p tw="font-bold text-2xl text-gray-400 m-0">Mapped by {leaderboard.levelAuthorName}</p>
|
<p tw="font-bold text-2xl text-gray-400 m-0">Mapped by {leaderboard.levelAuthorName}</p>
|
||||||
</div>
|
</ImageService.BaseImage>
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
width: 1200,
|
width: 1200,
|
||||||
@ -147,8 +136,5 @@ export class ImageService {
|
|||||||
emoji: "twemoji",
|
emoji: "twemoji",
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
imageCache.set(cacheKey, imageResponse);
|
|
||||||
return imageResponse;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user