feat(extra) : add catalog items and image export
CI / Verify (push) Successful in 1m1s
CI / Build immutable images and deploy (push) Successful in 2m19s

This commit is contained in:
2026-09-05 12:36:00 +07:00 Unverified
parent 5af38194ba
commit a4ecdf4fff
11 changed files with 3685 additions and 10 deletions
+25 -1
View File
@@ -148,6 +148,15 @@ async function getGuideContent(slug: string, publicOnly: boolean) {
]);
const extraIds = sections.filter((section) => section.kind === "extra").map((section) => section.id);
const extraRows = extraIds.length ? await getDb().select({ row: extraSectionRows, asset: media }).from(extraSectionRows).leftJoin(media, eq(media.id, extraSectionRows.mediaId)).where(inArray(extraSectionRows.sectionId, extraIds)).orderBy(asc(extraSectionRows.sortOrder)) : [];
const extraCharacterKeys = [...new Set(extraRows.flatMap(({ row: extraRow }) => extraRow.catalogItems.filter((item) => item.kind === "character").map((item) => item.key)))];
const extraWeaponKeys = [...new Set(extraRows.flatMap(({ row: extraRow }) => extraRow.catalogItems.filter((item) => item.kind === "weapon").map((item) => item.key)))];
const extraArtifactKeys = [...new Set(extraRows.flatMap(({ row: extraRow }) => extraRow.catalogItems.filter((item) => item.kind === "artifact").map((item) => item.key)))];
const [extraCharacters, extraWeapons, extraArtifacts] = await Promise.all([
extraCharacterKeys.length ? getDb().select().from(catalogCharacters).where(inArray(catalogCharacters.key, extraCharacterKeys)) : [],
extraWeaponKeys.length ? getDb().select().from(catalogWeapons).where(inArray(catalogWeapons.key, extraWeaponKeys)) : [],
extraArtifactKeys.length ? getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, extraArtifactKeys)) : [],
]);
const extraCatalogs = { character: extraCharacters, weapon: extraWeapons, artifact: extraArtifacts };
return {
...row.guide,
@@ -172,7 +181,22 @@ async function getGuideContent(slug: string, publicOnly: boolean) {
}),
})),
teams: teams.map((team) => ({ ...team, members: members.filter(({ member }) => member.teamId === team.id).map(({ member, character }) => { const weapon = memberWeapons.find((item) => item.key === member.weaponKey); const artifact = memberArtifacts.find((item) => item.key === member.artifactKey); const secondArtifact = memberArtifacts.find((item) => item.key === member.secondArtifactKey); return { ...member, character: catalogCharacterWithUrls(character), weapon: weapon ? { ...weapon, imageUrl: publicMediaUrl(weapon.imageKey) } : null, artifact: artifact ? { ...artifact, imageUrl: publicMediaUrl(artifact.imageKey) } : null, secondArtifact: secondArtifact ? { ...secondArtifact, imageUrl: publicMediaUrl(secondArtifact.imageKey) } : null }; }) })),
extras: sections.filter((section) => section.kind === "extra").map((section) => ({ ...section, rows: extraRows.filter(({ row: extraRow }) => extraRow.sectionId === section.id).map(({ row: extraRow, asset }) => ({ ...extraRow, imageUrl: asset ? publicMediaUrl(asset.objectKey) : null, imageWidth: asset?.width ?? null, imageHeight: asset?.height ?? null })) })),
extras: sections.filter((section) => section.kind === "extra").map((section) => ({ ...section, rows: extraRows.filter(({ row: extraRow }) => extraRow.sectionId === section.id).map(({ row: extraRow, asset }) => ({
...extraRow,
catalogItems: extraRow.catalogItems.map((reference) => {
const item = extraCatalogs[reference.kind].find((candidate) => candidate.key === reference.key);
return {
...reference,
name: item?.name ?? reference.key,
imageUrl: item ? publicMediaUrl(item.imageKey) : null,
rarity: item?.rarity ?? null,
available: Boolean(item),
};
}),
imageUrl: asset ? publicMediaUrl(asset.objectKey) : null,
imageWidth: asset?.width ?? null,
imageHeight: asset?.height ?? null,
})) })),
};
}
+27
View File
@@ -180,6 +180,33 @@ describe("structured guide validation", () => {
expect(result.data.sections[0].rows[0].body).toBe("<p><strong>Safe</strong></p>");
}
});
it("keeps ordered mixed catalog items in Extra rows and rejects duplicates", () => {
const row = {
mediaId: null,
heading: "Build options",
body: "",
percent: null,
backgroundColor: null,
highlights: [],
catalogItems: [
{ kind: "character", key: "10000089" },
{ kind: "weapon", key: "WEAPON_CLAYMORE" },
{ kind: "artifact", key: "ARTIFACT_SET" },
],
} as const;
const result = extraSectionSchema.safeParse({
expectedVersion: 1,
sections: [{ title: "Extra", rows: [row] }],
});
expect(result.success).toBe(true);
if (result.success) expect(result.data.sections[0].rows[0].catalogItems).toEqual(row.catalogItems);
expect(extraSectionSchema.safeParse({
expectedVersion: 1,
sections: [{ title: "Extra", rows: [{ ...row, catalogItems: [row.catalogItems[0], row.catalogItems[0]] }] }],
}).success).toBe(false);
});
});
describe("Team Calc", () => {
+7
View File
@@ -140,6 +140,13 @@ const extraRowSchema = z.strictObject({
body: z.string().trim().max(10_000),
percent: percent.nullable(),
backgroundColor: hexColor.nullable().default(null),
catalogItems: z.array(z.strictObject({
kind: z.enum(["character", "weapon", "artifact"]),
key: z.string().trim().min(1).max(64),
})).max(30).refine(
(items) => new Set(items.map((item) => `${item.kind}:${item.key}`)).size === items.length,
"รายการตัวละคร อาวุธ และอาร์ติแฟกต์ต้องไม่ซ้ำกัน",
).default([]),
highlights: z.array(z.strictObject({
text: z.string().trim().min(1).max(80),
color: hexColor,