feat : fix anything you can think
CI / Verify (push) Successful in 1m4s
CI / Build immutable images and deploy (push) Successful in 1m35s

This commit is contained in:
2026-08-30 12:03:17 +00:00 Unverified
parent 6003f408bd
commit 89c27e230f
6 changed files with 74 additions and 20 deletions
@@ -1,4 +1,6 @@
/* eslint-disable @next/next/no-img-element */
import { join } from "node:path";
import { ImageResponse } from "next/og";
import sharp from "sharp";
@@ -53,6 +55,14 @@ async function pngDataUrl(url: string): Promise<string | null> {
}
}
async function localPngDataUrl(fileName: string): Promise<string> {
const png = await sharp(join(process.cwd(), "public", "icon", fileName))
.resize(96, 96, { fit: "contain" })
.png()
.toBuffer();
return `data:image/png;base64,${png.toString("base64")}`;
}
export async function GET(
_request: Request,
context: RouteContext<"/api/guides/[guideId]/artifacts/image">,
@@ -61,7 +71,7 @@ export async function GET(
const guide = await getPublicArtifactExport(guideId);
if (!guide) return new Response("Guide not found", { status: 404 });
const [fonts, groupAssets] = await Promise.all([
const [fonts, groupAssets, statIconAssets] = await Promise.all([
anuphanFonts,
Promise.all(
guide.groups.map((group) =>
@@ -73,6 +83,11 @@ export async function GET(
),
),
),
Promise.all([
localPngDataUrl("Icon_Sands_of_Eon.webp"),
localPngDataUrl("Icon_Goblet_of_Eonothem.webp"),
localPngDataUrl("Icon_Circlet_of_Logos.webp"),
]),
]);
const profile = guide.profile;
@@ -81,7 +96,7 @@ export async function GET(
? 60 + profile.targets.length * 30 + (profile.talents.length ? 58 : 0) + (profile.note ? Math.max(48, Math.ceil(profile.note.length / 58) * 24) : 0)
: 0;
const rightHeight = profile
? 194 + (profile.substats.length ? 76 : 0) + (detailHeight > 60 ? detailHeight + 16 : 0) + 32
? 242 + (profile.substats.length ? 76 : 0) + (detailHeight > 60 ? detailHeight + 16 : 0) + 32
: 0;
const height = Math.max(520, 190 + Math.max(leftHeight, rightHeight));
const talents = normalizeTalentPriorities(profile?.talents ?? []);
@@ -162,11 +177,12 @@ export async function GET(
</div>
<div style={{ display: "flex" }}>
{[
{ label: "Sands", values: profile.sands },
{ label: "Goblet", values: profile.goblet },
{ label: "Circlet", values: profile.circlet },
{ label: "Sands", values: profile.sands, icon: statIconAssets[0] },
{ label: "Goblet", values: profile.goblet, icon: statIconAssets[1] },
{ label: "Circlet", values: profile.circlet, icon: statIconAssets[2] },
].map((stat) => (
<div key={stat.label} style={{ display: "flex", flex: 1, minHeight: 128, flexDirection: "column", alignItems: "center", gap: 10, borderRight: stat.label === "Circlet" ? "none" : "1px solid #34343d", padding: "16px 10px", textAlign: "center" }}>
<div key={stat.label} style={{ display: "flex", flex: 1, minHeight: 176, flexDirection: "column", alignItems: "center", gap: 10, borderRight: stat.label === "Circlet" ? "none" : "1px solid #34343d", padding: "16px 10px", textAlign: "center" }}>
<img src={stat.icon} alt="" width={48} height={48} style={{ objectFit: "contain" }} />
<div style={{ display: "flex", fontSize: 17, fontWeight: 700 }}>{stat.label}</div>
<div style={{ display: "flex", width: "100%", borderTop: "1px solid #34343d" }} />
<div style={{ display: "flex", fontSize: 14, lineHeight: 1.45 }}>
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -18,6 +18,7 @@ import { SiteHeader } from "@/components/public/site-header";
import { ElementIcon } from "@/components/public/element-icon";
import { LiveRefresh } from "@/components/live-refresh";
import { rarityGradientClass, rarityTextClass } from "@/lib/catalog/rarity";
import { isCustomTravelerKey } from "@/lib/catalog/travelers";
import type { PublicGuide } from "@/lib/guides/queries";
import { normalizeTalentPriorities, talentRelationSymbol } from "@/lib/guides/talents";
import { cn } from "@/lib/utils";
@@ -517,12 +518,12 @@ function TeamSection({ guide }: { guide: PublicGuide }) {
{team.members.map((member) => (
<div key={member.position} className="min-w-0 text-center">
<div className="grid grid-cols-[minmax(0,1fr)_2.75rem] gap-2">
<div className="relative aspect-square overflow-hidden rounded-lg bg-muted">
<div className={cn("relative aspect-square overflow-hidden rounded-lg bg-muted", rarityGradientClass(isCustomTravelerKey(member.character.key) ? 5 : member.character.rarity))}>
<Image
src={member.character.imageUrl}
alt={member.character.name}
fill
className="object-cover"
className="object-contain"
/>
</div>
<div className="flex flex-col gap-2">
+1 -1
View File
@@ -173,7 +173,7 @@ export async function processCatalogSyncJob(jobId: string): Promise<void> {
key: traveler.key,
name: traveler.name,
element: traveler.element,
rarity: 5,
rarity: traveler.rarity,
weaponType: "WEAPON_SWORD_ONE_HAND",
imageKey: `catalog/${version}/characters/${traveler.asset}`,
elementIconKey: `catalog/${version}/elements/${traveler.element.toLowerCase()}.png`,
+7 -1
View File
@@ -3,7 +3,7 @@ import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { customTravelers, isUpstreamTraveler } from "@/lib/catalog/travelers";
import { customTravelers, isCustomTravelerKey, isUpstreamTraveler } from "@/lib/catalog/travelers";
describe("custom Traveler catalog", () => {
it("blacklists every upstream Traveler spelling variant", () => {
@@ -24,4 +24,10 @@ describe("custom Traveler catalog", () => {
}));
expect(new Set(hashes).size).toBe(7);
});
it("marks every custom Traveler as a five-star character", () => {
expect(customTravelers.every((traveler) => traveler.rarity === 5)).toBe(true);
expect(isCustomTravelerKey("traveler_hydro")).toBe(true);
expect(isCustomTravelerKey("traveler_unknown")).toBe(false);
});
});
+11 -7
View File
@@ -1,13 +1,17 @@
export const customTravelers = [
{ key: "traveler_pyro", name: "Traveler Pyro", element: "Pyro", detailKey: "10000005_PYRO", asset: "traveler_pyro.webp" },
{ key: "traveler_hydro", name: "Traveler Hydro", element: "Hydro", detailKey: "10000005_HYDRO", asset: "traveler_hydro.webp" },
{ key: "traveler_anemo", name: "Traveler Anemo", element: "Anemo", detailKey: "10000005_ANEMO", asset: "traveler_anemo.webp" },
{ key: "traveler_geo", name: "Traveler Geo", element: "Geo", detailKey: "10000005_GEO", asset: "traveler_geo.webp" },
{ key: "traveler_electro", name: "Traveler Electro", element: "Electro", detailKey: "10000005_ELECTRO", asset: "traveler_electro.webp" },
{ key: "traveler_dendro", name: "Traveler Dendro", element: "Dendro", detailKey: "10000005_DENDRO", asset: "traveler_dendro.webp" },
{ key: "traveler_cryo", name: "Traveler Cryo", element: "Cryo", detailKey: "10000005_CRYO", asset: "traveler_cyro.webp" },
{ key: "traveler_pyro", name: "Traveler Pyro", element: "Pyro", rarity: 5, detailKey: "10000005_PYRO", asset: "traveler_pyro.webp" },
{ key: "traveler_hydro", name: "Traveler Hydro", element: "Hydro", rarity: 5, detailKey: "10000005_HYDRO", asset: "traveler_hydro.webp" },
{ key: "traveler_anemo", name: "Traveler Anemo", element: "Anemo", rarity: 5, detailKey: "10000005_ANEMO", asset: "traveler_anemo.webp" },
{ key: "traveler_geo", name: "Traveler Geo", element: "Geo", rarity: 5, detailKey: "10000005_GEO", asset: "traveler_geo.webp" },
{ key: "traveler_electro", name: "Traveler Electro", element: "Electro", rarity: 5, detailKey: "10000005_ELECTRO", asset: "traveler_electro.webp" },
{ key: "traveler_dendro", name: "Traveler Dendro", element: "Dendro", rarity: 5, detailKey: "10000005_DENDRO", asset: "traveler_dendro.webp" },
{ key: "traveler_cryo", name: "Traveler Cryo", element: "Cryo", rarity: 5, detailKey: "10000005_CRYO", asset: "traveler_cyro.webp" },
] as const;
export function isCustomTravelerKey(key: string): boolean {
return customTravelers.some((traveler) => traveler.key === key);
}
export function isUpstreamTraveler(name: string): boolean {
return name.trim().toLocaleLowerCase("en-US") === "traveler";
}