feat: add ListPage component for player profile display
- Implemented ListPage component to render player information including profile picture, namecard, nickname, level, world level, and signature. - Integrated asset loading for profile picture and namecard using assets.load. - Utilized Tailwind CSS for styling the layout and components.
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
import { file } from "bun";
|
import { file } from "bun";
|
||||||
|
import sansFontFile from "./open-sans.woff2" with { type: "file" };
|
||||||
import iconFontFile from "./shicon.woff" with { type: "file" };
|
import iconFontFile from "./shicon.woff" with { type: "file" };
|
||||||
import fontFile from "./shinshin.woff2" with { type: "file" };
|
import fontFile from "./shinshin.woff2" with { type: "file" };
|
||||||
|
|
||||||
export const font = file(fontFile);
|
export const font = file(fontFile);
|
||||||
export const iconFont = file(iconFontFile);
|
export const iconFont = file(iconFontFile);
|
||||||
|
export const sansFont = file(sansFontFile);
|
||||||
|
|||||||
Binary file not shown.
+42
-55
@@ -3,7 +3,14 @@ import { mkdir, readdir, rm } from "node:fs/promises";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { file, hash, write } from "bun";
|
import { file, hash, write } from "bun";
|
||||||
import { TTLCache } from "enka.ts";
|
import { TTLCache } from "enka.ts";
|
||||||
import { AvatarStore, EnkaLocs, RelicStore, WeaponStore } from "@/type/enka";
|
import {
|
||||||
|
AvatarStore,
|
||||||
|
EnkaLocs,
|
||||||
|
EnkaNamecards,
|
||||||
|
EnkaPfps,
|
||||||
|
RelicStore,
|
||||||
|
WeaponStore,
|
||||||
|
} from "@/type/enka";
|
||||||
import { Deduplicator } from "./dedupe";
|
import { Deduplicator } from "./dedupe";
|
||||||
import { createLogger } from "./logger";
|
import { createLogger } from "./logger";
|
||||||
|
|
||||||
@@ -17,20 +24,24 @@ export class AssetManager {
|
|||||||
private approximateSize = 0;
|
private approximateSize = 0;
|
||||||
|
|
||||||
private async getAssetList() {
|
private async getAssetList() {
|
||||||
const [avatars, weapons, relics] = await Promise.all([
|
const [avatars, weapons, relics, pfps, namecards] = await batchStore(
|
||||||
getEnkaAvatarStore(),
|
"avatars",
|
||||||
getEnkaWeaponStore(),
|
"weapons",
|
||||||
getEnkaRelicStore(),
|
"relics",
|
||||||
]);
|
"pfps",
|
||||||
|
"namecards",
|
||||||
|
);
|
||||||
return [
|
return [
|
||||||
...Object.values(avatars).flatMap((a) => [
|
...Object.values(avatars).flatMap((a) => [
|
||||||
...a.Consts,
|
...a.Consts,
|
||||||
...Object.values(a.Skills),
|
...Object.values(a.Skills),
|
||||||
...Object.values(a.Costumes ?? {}).map((c) => c.Art),
|
...Object.values(a.Costumes ?? {}).flatMap((c) => [c.Art, c.SideIcon]),
|
||||||
a.SideIconName.replace("AvatarIcon_Side", "Gacha_AvatarImg"),
|
a.SideIconName.replace("AvatarIcon_Side", "Gacha_AvatarImg"),
|
||||||
]),
|
]),
|
||||||
...Object.values(weapons).flatMap((w) => [w.Icon, w.AwakenIcon]),
|
...Object.values(weapons).flatMap((w) => [w.Icon, w.AwakenIcon]),
|
||||||
...Object.values(relics.Items).map((r) => r.Icon),
|
...Object.values(relics.Items).map((r) => r.Icon),
|
||||||
|
...Object.values(pfps).map((p) => p.IconPath),
|
||||||
|
...Object.values(namecards).map((p) => p.Icon),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,59 +166,35 @@ export class AssetManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEnkaStores() {
|
export const assets = new AssetManager();
|
||||||
return Promise.all([
|
|
||||||
getEnkaAvatarStore(),
|
export async function batchStore<K extends readonly (keyof StoreTypeMap)[]>(
|
||||||
getEnkaWeaponStore(),
|
...keys: K
|
||||||
getEnkaRelicStore(),
|
) {
|
||||||
getEnkaLocs(),
|
return Promise.all(keys.map(getEnkaStore)) as unknown as Promise<{
|
||||||
]);
|
[I in keyof K]: StoreTypeMap[K[I]];
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEnkaAvatarStore(): Promise<AvatarStore> {
|
type StoreTypeMap = {
|
||||||
const cached = cache.get("enkaAvatarStore");
|
avatars: AvatarStore;
|
||||||
if (cached) return cached as AvatarStore;
|
weapons: WeaponStore;
|
||||||
|
relics: RelicStore;
|
||||||
|
locs: EnkaLocs;
|
||||||
|
pfps: EnkaPfps;
|
||||||
|
namecards: EnkaNamecards;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getEnkaStore<K extends keyof StoreTypeMap>(
|
||||||
|
key: K,
|
||||||
|
): Promise<StoreTypeMap[K]> {
|
||||||
|
const cached = cache.get(`store.${key}`);
|
||||||
|
if (cached) return cached as StoreTypeMap[K];
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
"https://github.com/EnkaNetwork/API-docs/raw/refs/heads/master/store/gi/avatars.json",
|
`https://github.com/EnkaNetwork/API-docs/raw/refs/heads/master/store/gi/${key}.json`,
|
||||||
);
|
);
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
cache.set("enkaAvatarStore", data, 86400); // 24 hours
|
cache.set(`store.${key}`, data, 86400); // 24 hours
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getEnkaWeaponStore(): Promise<WeaponStore> {
|
|
||||||
const cached = cache.get("enkaWeaponStore");
|
|
||||||
if (cached) return cached as WeaponStore;
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
"https://github.com/EnkaNetwork/API-docs/raw/refs/heads/master/store/gi/weapons.json",
|
|
||||||
);
|
|
||||||
const data = await response.json();
|
|
||||||
cache.set("enkaWeaponStore", data, 86400); // 24 hours
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getEnkaRelicStore(): Promise<RelicStore> {
|
|
||||||
const cached = cache.get("enkaRelicStore");
|
|
||||||
if (cached) return cached as RelicStore;
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
"https://github.com/EnkaNetwork/API-docs/raw/refs/heads/master/store/gi/relics.json",
|
|
||||||
);
|
|
||||||
const data = await response.json();
|
|
||||||
cache.set("enkaRelicStore", data, 86400); // 24 hours
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getEnkaLocs(): Promise<EnkaLocs> {
|
|
||||||
const cached = cache.get("enkaLocs");
|
|
||||||
if (cached) return cached as EnkaLocs;
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
"https://github.com/EnkaNetwork/API-docs/raw/refs/heads/master/store/gi/locs.json",
|
|
||||||
);
|
|
||||||
const data = await response.json();
|
|
||||||
cache.set("enkaLocs", data, 86400); // 24 hours
|
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { t } from "elysia";
|
||||||
|
|
||||||
export type AvatarStore = Record<number, Avatar>;
|
export type AvatarStore = Record<number, Avatar>;
|
||||||
|
|
||||||
export interface Avatar {
|
export interface Avatar {
|
||||||
@@ -148,3 +150,37 @@ export const SubstatOrder: Record<string, number> = {
|
|||||||
FIGHT_PROP_HP: 9,
|
FIGHT_PROP_HP: 9,
|
||||||
FIGHT_PROP_DEFENSE: 10,
|
FIGHT_PROP_DEFENSE: 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type NumberLike = `${number}`;
|
||||||
|
export type CdnPath = `/ui/UI_${string}.jpg`;
|
||||||
|
|
||||||
|
export type EnkaPfps = Record<NumberLike, { IconPath: CdnPath }>;
|
||||||
|
|
||||||
|
export type EnkaNamecards = Record<NumberLike, { Icon: CdnPath }>;
|
||||||
|
|
||||||
|
export const Locale = t.Enum(
|
||||||
|
Object.fromEntries(
|
||||||
|
(
|
||||||
|
[
|
||||||
|
"zh-cn",
|
||||||
|
"zh-tw",
|
||||||
|
"de",
|
||||||
|
"en",
|
||||||
|
"es",
|
||||||
|
"fr",
|
||||||
|
"id",
|
||||||
|
"ja",
|
||||||
|
"ko",
|
||||||
|
"pt",
|
||||||
|
"ru",
|
||||||
|
"th",
|
||||||
|
"vi",
|
||||||
|
"tr",
|
||||||
|
"it",
|
||||||
|
] as const
|
||||||
|
).map((v) => [v, v]),
|
||||||
|
),
|
||||||
|
{
|
||||||
|
default: "en",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
+86
-42
@@ -22,17 +22,17 @@ import { star } from "@/assets/stars";
|
|||||||
import { TalentSvg } from "@/assets/talent";
|
import { TalentSvg } from "@/assets/talent";
|
||||||
import { WeaponGlazeSvg } from "@/assets/weapon-glaze";
|
import { WeaponGlazeSvg } from "@/assets/weapon-glaze";
|
||||||
import type { App } from "@/index";
|
import type { App } from "@/index";
|
||||||
import { AssetManager, getEnkaStores } from "@/lib/assets";
|
import { assets, batchStore } from "@/lib/assets";
|
||||||
import { Deduplicator } from "@/lib/dedupe";
|
import { Deduplicator } from "@/lib/dedupe";
|
||||||
import { cardErrorLocs } from "@/lib/locs";
|
import { cardErrorLocs } from "@/lib/locs";
|
||||||
import { createLogger } from "@/lib/logger";
|
import { createLogger } from "@/lib/logger";
|
||||||
import { mockData } from "@/lib/mock";
|
import { mockData } from "@/lib/mock";
|
||||||
import { buildDisplayRows } from "@/lib/rows";
|
import { buildDisplayRows } from "@/lib/rows";
|
||||||
import { ElementList, SubstatOrder } from "@/type/enka";
|
import { ElementList, Locale, SubstatOrder } from "@/type/enka";
|
||||||
import { PercentageFightProp } from "@/type/fightPropMap";
|
import { PercentageFightProp } from "@/type/fightPropMap";
|
||||||
import { ErrorPage } from "./error";
|
import { ErrorPage } from "./error";
|
||||||
|
import { ListPage } from "./list";
|
||||||
|
|
||||||
const assets = new AssetManager();
|
|
||||||
const cache = new TTLCache();
|
const cache = new TTLCache();
|
||||||
const deduper = new Deduplicator();
|
const deduper = new Deduplicator();
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ export function card(app: App) {
|
|||||||
.group("/card", (app) =>
|
.group("/card", (app) =>
|
||||||
app
|
app
|
||||||
.derive(() => ({
|
.derive(() => ({
|
||||||
logger: createLogger(),
|
log: createLogger(),
|
||||||
}))
|
}))
|
||||||
// FE still uses this endpoint for some reason(aka lazy to change)
|
// FE still uses this endpoint for some reason(aka lazy to change)
|
||||||
.head("/genshin/:uid/:char/preload", async ({ params: { uid } }) =>
|
.head("/genshin/:uid/:char/preload", async ({ params: { uid } }) =>
|
||||||
@@ -69,15 +69,21 @@ export function card(app: App) {
|
|||||||
)
|
)
|
||||||
.head(
|
.head(
|
||||||
"/genshin/:uid/preload",
|
"/genshin/:uid/preload",
|
||||||
async ({ params: { uid }, logger }) => {
|
async ({ params: { uid }, log }) => {
|
||||||
logger`Preload ${uid}`;
|
log`Preload ${uid}`;
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
deduper.acquire("stores", () =>
|
deduper.acquire("stores", () =>
|
||||||
getEnkaStores().finally(() => logger`Stores loaded`),
|
batchStore(
|
||||||
|
"avatars",
|
||||||
|
"locs",
|
||||||
|
"relics",
|
||||||
|
"weapons",
|
||||||
|
"pfps",
|
||||||
|
).finally(() => log`Stores loaded`),
|
||||||
),
|
),
|
||||||
deduper.acquire(`enka-${uid}`, () =>
|
deduper.acquire(`enka-${uid}`, () =>
|
||||||
fetchEnka(uid, false).finally(() => logger`Enka data loaded`),
|
fetchEnka(uid, false).finally(() => log`Enka data loaded`),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -95,17 +101,78 @@ export function card(app: App) {
|
|||||||
tags: ["Card"],
|
tags: ["Card"],
|
||||||
description: "Preload data required to render a card.",
|
description: "Preload data required to render a card.",
|
||||||
},
|
},
|
||||||
afterResponse: ({ logger }) => logger`Data preload complete`,
|
afterResponse: ({ log }) => log`Data preload complete`,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.get(
|
||||||
|
"/genshin/:uid",
|
||||||
|
async ({ params: { uid }, query: { debug }, log }) => {
|
||||||
|
const [[avatarStore, locs, pfps, namecards], data] =
|
||||||
|
await Promise.all([
|
||||||
|
deduper.acquire("stores", () =>
|
||||||
|
batchStore("avatars", "locs", "pfps", "namecards").finally(
|
||||||
|
() => log`Stores loaded`,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
feature("API") || uid !== "775956514"
|
||||||
|
? deduper.acquire(`enka-${uid}`, () =>
|
||||||
|
fetchEnka(uid, true).finally(() => log`Enka data loaded`),
|
||||||
|
)
|
||||||
|
: mockData,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (debug) return { data };
|
||||||
|
|
||||||
|
return ListPage(
|
||||||
|
data,
|
||||||
|
pfps[
|
||||||
|
`${data.playerInfo.profilePicture.id ?? 1}`
|
||||||
|
].IconPath.replace("_Circle", ""),
|
||||||
|
namecards[`${data.playerInfo.namecardId ?? 210001}`].Icon,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: t.Object({
|
||||||
|
uid: t.String({
|
||||||
|
examples: ["775956514"],
|
||||||
|
description: "Genshin Impact UID",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
query: t.Partial(
|
||||||
|
t.Object(
|
||||||
|
{
|
||||||
|
error: t.Enum(
|
||||||
|
{ http: "http", card: "card" },
|
||||||
|
{
|
||||||
|
default: "http",
|
||||||
|
description:
|
||||||
|
"Type of error to return when card fails to render.",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
lang: Locale,
|
||||||
|
debug: t.Boolean({
|
||||||
|
description: "Return debug information instead of card.",
|
||||||
|
default: false,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{ additionalProperties: true },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
detail: {
|
||||||
|
tags: ["Card"],
|
||||||
|
description:
|
||||||
|
"Generate a Genshin Impact character list card image from a UID.",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.get(
|
.get(
|
||||||
"/genshin/:uid/:char",
|
"/genshin/:uid/:char",
|
||||||
async ({
|
async ({
|
||||||
params: { uid, char: charId },
|
params: { uid, char: charId },
|
||||||
logger,
|
log,
|
||||||
query: { debug, h: heightRaw = 1024, w, error: errorType, lang },
|
query: { debug, h: heightRaw = 1024, w, error: errorType, lang },
|
||||||
}) => {
|
}) => {
|
||||||
logger`Rendering ${uid}/${charId}`;
|
log`Rendering ${uid}/${charId}`;
|
||||||
const error =
|
const error =
|
||||||
errorType === "card"
|
errorType === "card"
|
||||||
? (_: number, message: string) => ErrorPage(message)
|
? (_: number, message: string) => ErrorPage(message)
|
||||||
@@ -116,12 +183,14 @@ export function card(app: App) {
|
|||||||
const [[avatarStore, weaponStore, relicStore, locs], data] =
|
const [[avatarStore, weaponStore, relicStore, locs], data] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
deduper.acquire("stores", () =>
|
deduper.acquire("stores", () =>
|
||||||
getEnkaStores().finally(() => logger`Stores loaded`),
|
batchStore("avatars", "weapons", "relics", "locs").finally(
|
||||||
|
() => log`Stores loaded`,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
feature("API") || uid !== "775956514"
|
feature("API") || uid !== "775956514"
|
||||||
? deduper.acquire(`enka-${uid}`, () =>
|
? deduper.acquire(`enka-${uid}`, () =>
|
||||||
fetchEnka(uid, false).finally(
|
fetchEnka(uid, false).finally(
|
||||||
() => logger`Enka data loaded`,
|
() => log`Enka data loaded`,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: mockData,
|
: mockData,
|
||||||
@@ -285,10 +354,10 @@ export function card(app: App) {
|
|||||||
src: string;
|
src: string;
|
||||||
data: ArrayBuffer | Buffer;
|
data: ArrayBuffer | Buffer;
|
||||||
}>[]);
|
}>[]);
|
||||||
logger`Image resources loaded\n${resources.map((r) => `- ${r.src} ${(r.data.byteLength / 1000).toFixed(2)}Kb`).join("\n")}`;
|
log`Image resources loaded\n${resources.map((r) => `- ${r.src} ${(r.data.byteLength / 1000).toFixed(2)}Kb`).join("\n")}`;
|
||||||
|
|
||||||
if (debug) {
|
if (debug) {
|
||||||
logger`Debug endpoint, returning JSON`;
|
log`Debug endpoint, returning JSON`;
|
||||||
return JSON.stringify(
|
return JSON.stringify(
|
||||||
{
|
{
|
||||||
charLvl,
|
charLvl,
|
||||||
@@ -757,32 +826,7 @@ export function card(app: App) {
|
|||||||
"Type of error to return when card fails to render.",
|
"Type of error to return when card fails to render.",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
lang: t.Enum(
|
lang: Locale,
|
||||||
Object.fromEntries(
|
|
||||||
(
|
|
||||||
[
|
|
||||||
"zh-cn",
|
|
||||||
"zh-tw",
|
|
||||||
"de",
|
|
||||||
"en",
|
|
||||||
"es",
|
|
||||||
"fr",
|
|
||||||
"id",
|
|
||||||
"ja",
|
|
||||||
"ko",
|
|
||||||
"pt",
|
|
||||||
"ru",
|
|
||||||
"th",
|
|
||||||
"vi",
|
|
||||||
"tr",
|
|
||||||
"it",
|
|
||||||
] as const
|
|
||||||
).map((v) => [v, v]),
|
|
||||||
),
|
|
||||||
{
|
|
||||||
default: "en",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
h: t.Number({
|
h: t.Number({
|
||||||
default: 1024,
|
default: 1024,
|
||||||
description: "Height of card.",
|
description: "Height of card.",
|
||||||
@@ -815,7 +859,7 @@ export function card(app: App) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
afterResponse: ({ logger }) => logger`Rendering completed`,
|
afterResponse: ({ log }) => log`Rendering completed`,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { ResolvedRoot } from "enka.ts";
|
||||||
|
import { assets } from "@/lib/assets";
|
||||||
|
import { CardResponse } from "./response";
|
||||||
|
|
||||||
|
export async function ListPage(
|
||||||
|
{ playerInfo: p, uid }: ResolvedRoot,
|
||||||
|
pfp: string,
|
||||||
|
namecard: string,
|
||||||
|
) {
|
||||||
|
return CardResponse(
|
||||||
|
<div tw="flex font-[Open_Sans]">
|
||||||
|
{/* left bar */}
|
||||||
|
<div tw="flex flex-col justify-between p-[1em] w-[37.5%]">
|
||||||
|
{/* profile */}
|
||||||
|
<div tw="flex gap-[1em] relative">
|
||||||
|
<div tw="absolute -top-1 -left-1 -bottom-1 -right-1 overflow-hidden rounded-[.3em] opacity-50">
|
||||||
|
<img src="/namecard" alt="namecard" tw="object-cover" />
|
||||||
|
</div>
|
||||||
|
<div tw="overflow-hidden rounded-[.7em] bg-black/25 shrink-0">
|
||||||
|
<img
|
||||||
|
src="/pfp"
|
||||||
|
alt="pfp"
|
||||||
|
tw="object-cover scale-120 w-[10em] h-[10em]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div tw="flex flex-col justify-evenly py-[.5em] min-w-0">
|
||||||
|
<div tw="text-[1.4em] font-[ShinShin]">{p.nickname}</div>
|
||||||
|
<div tw="flex items-center gap-[.4em] opacity-70 font-bold text-[1.2em] tracking-widest">
|
||||||
|
AR {p.level}
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 100 100"
|
||||||
|
tw="h-[.75em] w-[.75em]"
|
||||||
|
>
|
||||||
|
<title>doritos</title>
|
||||||
|
<polygon points="50,10 90,90 10,90" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
WL {p.worldLevel}
|
||||||
|
</div>
|
||||||
|
<div tw="grow">
|
||||||
|
<div tw="opacity-70 pt-[.5em] text-[1.1em] leading-none">
|
||||||
|
{p.signature}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
[
|
||||||
|
{ src: "/pfp", data: assets.load(pfp) },
|
||||||
|
{ src: "/namecard", data: assets.load(namecard) },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
+22
-6
@@ -1,27 +1,43 @@
|
|||||||
import ImageResponse from "@takumi-rs/image-response";
|
import ImageResponse from "@takumi-rs/image-response";
|
||||||
|
import { MaybePromise } from "bun";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
import { font, iconFont } from "@/assets/fonts";
|
import { font, iconFont, sansFont } from "@/assets/fonts";
|
||||||
|
|
||||||
export async function CardResponse(children: ReactNode) {
|
export async function CardResponse(
|
||||||
|
children: ReactNode,
|
||||||
|
assets: MaybePromise<{
|
||||||
|
src: string;
|
||||||
|
data: MaybePromise<ArrayBuffer | Buffer>;
|
||||||
|
}>[] = [],
|
||||||
|
) {
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<div tw="text-white bg-[#2f3136] w-full h-full flex flex-col">
|
<div tw="text-white bg-[#131416] w-full h-full flex flex-col rounded-[.5em]">
|
||||||
{children}
|
{children}
|
||||||
<div tw="grow" />
|
<div tw="grow" />
|
||||||
<div tw="ml-[.5em] flex items-center">
|
<div tw="ml-[.5em] flex items-center">
|
||||||
<div tw="text-[2em] font-[shicon] pb-[.1em] -mr-[.3em] text-black/50">
|
<div tw="text-[2em] font-[shicon] pb-[.1em] -mr-[.3em] text-white/25">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div tw="flex flex-col">
|
<div tw="flex flex-col">
|
||||||
<div tw="leading-none text-white/50">Powered By</div>
|
<div tw="leading-none text-white/75">Powered By</div>
|
||||||
<div tw="text-[1.5em] leading-[.8]">Enka.Network</div>
|
<div tw="text-[1.5em] leading-[.8]">Enka.Network</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
{
|
{
|
||||||
fonts: [await font.arrayBuffer(), await iconFont.arrayBuffer()],
|
fonts: [
|
||||||
|
await font.arrayBuffer(),
|
||||||
|
await sansFont.arrayBuffer(),
|
||||||
|
await iconFont.arrayBuffer(),
|
||||||
|
],
|
||||||
format: "webp",
|
format: "webp",
|
||||||
// same ratio as card
|
// same ratio as card
|
||||||
width: 1228,
|
width: 1228,
|
||||||
|
fetchedResources: await Promise.all(assets).then((list) =>
|
||||||
|
Promise.all(
|
||||||
|
list.map(async (a) => ({ src: a.src, data: await a.data })),
|
||||||
|
),
|
||||||
|
),
|
||||||
height: 512,
|
height: 512,
|
||||||
// drawDebugBorder: true,
|
// drawDebugBorder: true,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user