feat(catalog): color equipment by rarity
CI / Verify (push) Successful in 1m0s
CI / Build immutable images and deploy (push) Successful in 3m26s

This commit is contained in:
2026-08-30 06:09:19 +00:00 Unverified
parent 890fb9385a
commit d0e2a95350
10 changed files with 3544 additions and 20 deletions
+3 -9
View File
@@ -2,6 +2,7 @@
import { ImageResponse } from "next/og";
import sharp from "sharp";
import { rarityGradient } from "@/lib/catalog/rarity";
import { getPublicTeamExport } from "@/lib/guides/queries";
const anuphanFonts = Promise.all(
@@ -28,13 +29,6 @@ function amount(value: number): string {
return new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 }).format(value);
}
function weaponBackground(rarity: number | null): string {
if (rarity === 5) return "linear-gradient(90deg, #8a5a00 0%, #0f0800 100%)";
if (rarity === 4) return "linear-gradient(90deg, #a300b8 0%, #000000 100%)";
if (rarity === 3) return "linear-gradient(90deg, #00a0b5 0%, #000000 100%)";
return "#17383d";
}
function contentDisposition(teamName: string): string {
const displayName = teamName.trim() || "team";
const fallback = displayName
@@ -133,7 +127,7 @@ export async function GET(
)}
<div style={{ display: "flex", gap: 6 }}>
{member.weapon && memberAssets[index].weapon ? (
<div style={{ position: "relative", display: "flex", width: 50, height: 50, overflow: "hidden", borderRadius: 10, background: weaponBackground(member.weapon.rarity) }}>
<div style={{ position: "relative", display: "flex", width: 50, height: 50, overflow: "hidden", borderRadius: 10, background: rarityGradient(member.weapon.rarity) }}>
<img src={memberAssets[index].weapon} alt="" width={50} height={50} style={{ objectFit: "contain" }} />
<div style={{ position: "absolute", top: 2, right: 2, display: "flex", borderRadius: 4, background: "#f4fbfa", color: "#06171b", padding: "1px 4px", fontSize: 11, fontWeight: 700 }}>
R{member.refinement}
@@ -141,7 +135,7 @@ export async function GET(
</div>
) : null}
{member.artifact && memberAssets[index].artifact ? (
<div style={{ display: "flex", width: 50, height: 50, overflow: "hidden", borderRadius: 10, background: "#17383d" }}>
<div style={{ display: "flex", width: 50, height: 50, overflow: "hidden", borderRadius: 10, background: rarityGradient(member.artifact.rarity) }}>
<img src={memberAssets[index].artifact} alt="" width={50} height={50} style={{ objectFit: "contain" }} />
</div>
) : null}
File diff suppressed because one or more lines are too long
+4 -3
View File
@@ -9,6 +9,7 @@ import { Kbd } from "@/components/ui/kbd";
import { Separator } from "@/components/ui/separator";
import { SiteHeader } from "@/components/public/site-header";
import { LiveRefresh } from "@/components/live-refresh";
import { rarityGradientClass } from "@/lib/catalog/rarity";
import type { PublicGuide } from "@/lib/guides/queries";
import { cn } from "@/lib/utils";
@@ -44,14 +45,14 @@ function GuideSection({ guide, section }: { guide: PublicGuide; section: string
function metric(value: string | null) { return value === null ? null : `${Number(value).toLocaleString(undefined, { maximumFractionDigits: 2 })}%`; }
function WeaponSection({ guide }: { guide: PublicGuide }) { return <section className="flex flex-col gap-6"><header className="text-center"><p className="text-sm font-medium text-primary">อาวุธที่แนะนำ</p><h1 className="font-heading text-4xl font-semibold">Weapons</h1></header><div className="grid gap-3 lg:grid-cols-2">{guide.weapons.map((row, index) => <Card key={row.id} size="sm" className={cn(row.weapon.rarity === 5 && "bg-[linear-gradient(90deg,rgba(138,90,0,1)_0%,rgba(15,8,0,1)_100%)]", row.weapon.rarity === 4 && "bg-gradient-to-r from-[#a300b8] to-black", row.weapon.rarity === 3 && "bg-[linear-gradient(90deg,rgba(0,160,181,1)_0%,rgba(0,0,0,1)_100%)]")}><CardContent className="flex items-center gap-4"><div className="relative size-16 shrink-0 overflow-hidden rounded-lg"><Image src={row.weapon.imageUrl} alt={row.weapon.name} fill className="object-contain" /></div><div className="min-w-0 flex-1"><p className="font-semibold">{index + 1}. {row.weapon.name} R{row.refinement}</p></div>{row.overallPercent ? <div className="text-right"><p className="text-xs text-muted-foreground">ดาเมทโดยรวม</p><p className="text-2xl font-semibold">{metric(row.overallPercent)}</p></div> : null}{row.personalPercent ? <div className="text-right"><p className="text-xs text-muted-foreground">ดาเมทส่วนตัว</p><p className="text-2xl font-semibold">{metric(row.personalPercent)}</p></div> : null}</CardContent></Card>)}</div></section>; }
function WeaponSection({ guide }: { guide: PublicGuide }) { return <section className="flex flex-col gap-6"><header className="text-center"><p className="text-sm font-medium text-primary">อาวุธที่แนะนำ</p><h1 className="font-heading text-4xl font-semibold">Weapons</h1></header><div className="grid gap-3 lg:grid-cols-2">{guide.weapons.map((row, index) => <Card key={row.id} size="sm" className={cn(rarityGradientClass(row.weapon.rarity))}><CardContent className="flex items-center gap-4"><div className="relative size-16 shrink-0 overflow-hidden rounded-lg"><Image src={row.weapon.imageUrl} alt={row.weapon.name} fill className="object-contain" /></div><div className="min-w-0 flex-1"><p className="font-semibold">{index + 1}. {row.weapon.name} R{row.refinement}</p></div>{row.overallPercent ? <div className="text-right"><p className="text-xs text-muted-foreground">ดาเมทโดยรวม</p><p className="text-2xl font-semibold">{metric(row.overallPercent)}</p></div> : null}{row.personalPercent ? <div className="text-right"><p className="text-xs text-muted-foreground">ดาเมทส่วนตัว</p><p className="text-2xl font-semibold">{metric(row.personalPercent)}</p></div> : null}</CardContent></Card>)}</div></section>; }
function ArtifactSection({ guide }: { guide: PublicGuide }) { const profile = guide.artifactProfile; return <section className="flex flex-col gap-6"><header className="text-center"><p className="text-sm font-medium text-primary">อาร์ติแฟกต์และสเตตัสที่แนะนำ</p><h1 className="font-heading text-4xl font-semibold">Artifacts & Stats</h1></header><div className="grid gap-5 lg:grid-cols-[1.2fr_1fr]"> <div className="flex flex-col gap-5">{guide.artifactGroups.map((group) => <Card key={group.id}><CardHeader><CardTitle>{group.name}</CardTitle></CardHeader><CardContent className="flex flex-col gap-3">{group.rows.map((row, index) => <div key={row.id} className="flex items-center gap-3"><div className="flex -space-x-3"><Image src={row.first.imageUrl} alt={row.first.name} width={56} height={56} className="rounded-lg border bg-muted" />{row.second ? <Image src={row.second.imageUrl} alt={row.second.name} width={56} height={56} className="rounded-lg border bg-muted" /> : null}</div><div className="min-w-0 flex-1"><p className="font-medium">{index + 1}. {row.first.name}{row.second ? ` + ${row.second.name}` : ""}</p></div><p className="text-2xl font-semibold">{metric(row.percent)}</p></div>)}</CardContent></Card>)}</div>{profile ? <Card><CardHeader><CardTitle>Recommended Stats</CardTitle></CardHeader><CardContent className="flex flex-col gap-5"><StatLine label="Sands" values={profile.sands} /><StatLine label="Goblet" values={profile.goblet} /><StatLine label="Circlet" values={profile.circlet} /><Separator /><StatLine label="Substats" values={profile.substats} />{profile.targets.length ? <div><p className="mb-2 text-sm font-medium">Target Stats</p>{profile.targets.map((target) => <div key={`${target.stat}-${target.value}`} className="flex justify-between py-1 text-sm"><span>{target.stat}</span><strong>{target.value}</strong></div>)}</div> : null}{profile.talents.length ? <div><p className="mb-2 text-sm font-medium">Talent Priority</p><div className="flex gap-2">{[...profile.talents].sort((a,b) => a.priority-b.priority).map((talent) => <Badge key={talent.type} variant="secondary">{talent.priority}. {talent.type}{talent.level ? ` ${talent.level}` : ""}</Badge>)}</div></div> : null}{profile.note ? <p className="text-sm leading-6 text-muted-foreground">{profile.note}</p> : null}</CardContent></Card> : null}</div></section>; }
function ArtifactSection({ guide }: { guide: PublicGuide }) { const profile = guide.artifactProfile; return <section className="flex flex-col gap-6"><header className="text-center"><p className="text-sm font-medium text-primary">อาร์ติแฟกต์และสเตตัสที่แนะนำ</p><h1 className="font-heading text-4xl font-semibold">Artifacts & Stats</h1></header><div className="grid gap-5 lg:grid-cols-[1.2fr_1fr]"> <div className="flex flex-col gap-5">{guide.artifactGroups.map((group) => <Card key={group.id}><CardHeader><CardTitle>{group.name}</CardTitle></CardHeader><CardContent className="flex flex-col gap-3">{group.rows.map((row, index) => <div key={row.id} className="flex items-center gap-3"><div className="flex -space-x-3"><div className={cn("relative size-14 overflow-hidden rounded-lg border", rarityGradientClass(row.first.rarity))}><Image src={row.first.imageUrl} alt={row.first.name} fill className="object-contain" /></div>{row.second ? <div className={cn("relative size-14 overflow-hidden rounded-lg border", rarityGradientClass(row.second.rarity))}><Image src={row.second.imageUrl} alt={row.second.name} fill className="object-contain" /></div> : null}</div><div className="min-w-0 flex-1"><p className="font-medium">{index + 1}. {row.first.name}{row.second ? ` + ${row.second.name}` : ""}</p></div><p className="text-2xl font-semibold">{metric(row.percent)}</p></div>)}</CardContent></Card>)}</div>{profile ? <Card><CardHeader><CardTitle>Recommended Stats</CardTitle></CardHeader><CardContent className="flex flex-col gap-5"><StatLine label="Sands" values={profile.sands} /><StatLine label="Goblet" values={profile.goblet} /><StatLine label="Circlet" values={profile.circlet} /><Separator /><StatLine label="Substats" values={profile.substats} />{profile.targets.length ? <div><p className="mb-2 text-sm font-medium">Target Stats</p>{profile.targets.map((target) => <div key={`${target.stat}-${target.value}`} className="flex justify-between py-1 text-sm"><span>{target.stat}</span><strong>{target.value}</strong></div>)}</div> : null}{profile.talents.length ? <div><p className="mb-2 text-sm font-medium">Talent Priority</p><div className="flex gap-2">{[...profile.talents].sort((a,b) => a.priority-b.priority).map((talent) => <Badge key={talent.type} variant="secondary">{talent.priority}. {talent.type}{talent.level ? ` ${talent.level}` : ""}</Badge>)}</div></div> : null}{profile.note ? <p className="text-sm leading-6 text-muted-foreground">{profile.note}</p> : null}</CardContent></Card> : null}</div></section>; }
function StatLine({ label, values }: { label: string; values: string[] }) { return <div><p className="text-sm text-muted-foreground">{label}</p><div className="mt-1 flex flex-wrap gap-2">{values.map((value) => <Badge key={value} variant="outline">{value}</Badge>)}</div></div>; }
function ConstellationSection({ guide }: { guide: PublicGuide }) { return <section className="flex flex-col gap-6"><header className="text-center"><p className="text-sm font-medium text-primary">กลุ่มดาว</p><h1 className="font-heading text-4xl font-semibold">Constellations</h1></header><div className="grid gap-4 md:grid-cols-2">{guide.constellations.map((row) => <Card key={row.level}><CardHeader><CardTitle className="text-3xl text-white">{metric(row.cumulativePercent)}</CardTitle><CardDescription className="text-base font-medium text-white">C{row.level} · {row.name}</CardDescription>{row.iconUrl ? <CardAction><Image src={row.iconUrl} alt="" width={48} height={48} className="rounded-lg bg-muted" /></CardAction> : null}</CardHeader><CardContent><p className="leading-7">{row.detail}</p></CardContent></Card>)}</div></section>; }
function TeamSection({ guide }: { guide: PublicGuide }) { return <section className="flex flex-col gap-6"><header className="text-center"><p className="text-sm font-medium text-primary">คำนวณความเสียหาย</p><h1 className="font-heading text-4xl font-semibold">Team Calc</h1></header><div className="grid gap-5 xl:grid-cols-2">{guide.teams.map((team) => { const total = team.members.reduce((sum, member) => sum + Number(member.damage), 0); const seconds = Number(team.rotationSeconds); return <Card key={team.id}><CardHeader><CardTitle>{team.title}</CardTitle><CardDescription>{seconds}s rotation</CardDescription><CardAction><a href={`/api/teams/${team.id}/image`} download className={buttonVariants({ variant: "outline", size: "sm" })}><DownloadIcon data-icon="inline-start" />Export image</a></CardAction></CardHeader><CardContent className="flex flex-col gap-4"><div className="grid grid-cols-2 gap-3 sm:grid-cols-4">{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"><Image src={member.character.imageUrl} alt={member.character.name} fill className="object-cover" /></div><div className="flex flex-col gap-2">{member.weapon ? <div className={cn("relative aspect-square overflow-hidden rounded-md bg-muted", member.weapon.rarity === 5 && "bg-[linear-gradient(90deg,rgba(138,90,0,1)_0%,rgba(15,8,0,1)_100%)]", member.weapon.rarity === 4 && "bg-gradient-to-r from-[#a300b8] to-black", member.weapon.rarity === 3 && "bg-[linear-gradient(90deg,rgba(0,160,181,1)_0%,rgba(0,0,0,1)_100%)]")}><Image src={member.weapon.imageUrl} alt={member.weapon.name} fill className="object-contain" /><Kbd className="absolute top-0 right-0">R{member.refinement}</Kbd></div> : <div className="aspect-square rounded-md border bg-muted" />}{member.artifact ? <div className="relative aspect-square overflow-hidden rounded-md bg-muted"><Image src={member.artifact.imageUrl} alt={member.artifact.name} fill className="object-contain" /></div> : <div className="aspect-square rounded-md border bg-muted" />}</div></div><p className="mt-2 truncate text-sm font-medium">C{member.constellation} {member.character.name}</p><p className="text-sm font-semibold">{Number(member.damage).toLocaleString(undefined, { maximumFractionDigits: 2 })}</p><p className="text-xs text-muted-foreground">{total ? (Number(member.damage) / total * 100).toFixed(2) : "0.00"}%</p></div>)}</div><Separator /><div className="flex items-end justify-between"><div><p className="text-sm text-muted-foreground">Total damage</p><p className="font-semibold">{total.toLocaleString()}</p></div><div className="text-right"><p className="text-sm text-muted-foreground">Total DPS</p><p className="text-3xl font-semibold text-primary">{seconds > 0 ? (total / seconds).toLocaleString(undefined, { maximumFractionDigits: 0 }) : "0"}</p></div></div><p className="rounded-lg bg-muted p-3 text-sm leading-6">{team.combo}</p></CardContent></Card>; })}</div></section>; }
function TeamSection({ guide }: { guide: PublicGuide }) { return <section className="flex flex-col gap-6"><header className="text-center"><p className="text-sm font-medium text-primary">คำนวณความเสียหาย</p><h1 className="font-heading text-4xl font-semibold">Team Calc</h1></header><div className="grid gap-5 xl:grid-cols-2">{guide.teams.map((team) => { const total = team.members.reduce((sum, member) => sum + Number(member.damage), 0); const seconds = Number(team.rotationSeconds); return <Card key={team.id}><CardHeader><CardTitle>{team.title}</CardTitle><CardDescription>{seconds}s rotation</CardDescription><CardAction><a href={`/api/teams/${team.id}/image`} download className={buttonVariants({ variant: "outline", size: "sm" })}><DownloadIcon data-icon="inline-start" />Export image</a></CardAction></CardHeader><CardContent className="flex flex-col gap-4"><div className="grid grid-cols-2 gap-3 sm:grid-cols-4">{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"><Image src={member.character.imageUrl} alt={member.character.name} fill className="object-cover" /></div><div className="flex flex-col gap-2">{member.weapon ? <div className={cn("relative aspect-square overflow-hidden rounded-md bg-muted", rarityGradientClass(member.weapon.rarity))}><Image src={member.weapon.imageUrl} alt={member.weapon.name} fill className="object-contain" /><Kbd className="absolute top-0 right-0">R{member.refinement}</Kbd></div> : <div className="aspect-square rounded-md border bg-muted" />}{member.artifact ? <div className={cn("relative aspect-square overflow-hidden rounded-md bg-muted", rarityGradientClass(member.artifact.rarity))}><Image src={member.artifact.imageUrl} alt={member.artifact.name} fill className="object-contain" /></div> : <div className="aspect-square rounded-md border bg-muted" />}</div></div><p className="mt-2 truncate text-sm font-medium">C{member.constellation} {member.character.name}</p><p className="text-sm font-semibold">{Number(member.damage).toLocaleString(undefined, { maximumFractionDigits: 2 })}</p><p className="text-xs text-muted-foreground">{total ? (Number(member.damage) / total * 100).toFixed(2) : "0.00"}%</p></div>)}</div><Separator /><div className="flex items-end justify-between"><div><p className="text-sm text-muted-foreground">Total damage</p><p className="font-semibold">{total.toLocaleString()}</p></div><div className="text-right"><p className="text-sm text-muted-foreground">Total DPS</p><p className="text-3xl font-semibold text-primary">{seconds > 0 ? (total / seconds).toLocaleString(undefined, { maximumFractionDigits: 0 }) : "0"}</p></div></div><p className="rounded-lg bg-muted p-3 text-sm leading-6">{team.combo}</p></CardContent></Card>; })}</div></section>; }
function ExtraSection({ extra }: { extra: PublicGuide["extras"][number] }) { return <section className="flex flex-col gap-6"><header className="text-center"><p className="text-sm font-medium text-primary">Extra Guide</p><h1 className="font-heading text-4xl font-semibold">{extra.title}</h1></header><div className="grid gap-5 md:grid-cols-2">{extra.rows.map((row) => <Card key={row.id} className="overflow-hidden py-0">{row.imageUrl ? <div className="relative aspect-video bg-muted"><Image src={row.imageUrl} alt={row.heading} fill className="object-cover" /></div> : null}<CardHeader className="pt-5"><CardTitle>{row.heading}</CardTitle><CardDescription>{row.body}</CardDescription>{row.percent ? <Badge>{metric(row.percent)}</Badge> : null}</CardHeader></Card>)}</div></section>; }
+1
View File
@@ -489,6 +489,7 @@ export const catalogArtifacts = catalogSchema.table(
.references(() => catalogVersions.version, { onDelete: "cascade" }),
key: varchar("key", { length: 64 }).notNull(),
name: text("name").notNull(),
rarity: integer("rarity"),
imageKey: text("image_key").notNull(),
},
(table) => [
+1
View File
@@ -0,0 +1 @@
ALTER TABLE "catalog"."catalog_artifact" ADD COLUMN "rarity" integer;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -43,6 +43,13 @@
"when": 1788067778682,
"tag": "0005_pink_carlie_cooper",
"breakpoints": true
},
{
"idx": 6,
"version": "7",
"when": 1788070114887,
"tag": "0006_overconfident_drax",
"breakpoints": true
}
]
}
+18
View File
@@ -0,0 +1,18 @@
import { describe, expect, it } from "vitest";
import { rarityGradient, rarityGradientClass } from "@/lib/catalog/rarity";
describe("catalog rarity backgrounds", () => {
it("maps every weapon and artifact rarity to its Tailwind gradient", () => {
expect(rarityGradientClass(1)).toContain("rgba(23,25,28,1)");
expect(rarityGradientClass(2)).toContain("rgba(11,43,24,1)");
expect(rarityGradientClass(3)).toContain("rgba(0,160,181,1)");
expect(rarityGradientClass(4)).toContain("from-[#a300b8]");
expect(rarityGradientClass(5)).toContain("rgba(138,90,0,1)");
});
it("uses the neutral equipment background when rarity is unavailable", () => {
expect(rarityGradient(null)).toBe("#17383d");
expect(rarityGradientClass(undefined)).toBeUndefined();
});
});
+23
View File
@@ -0,0 +1,23 @@
const rarityGradientClasses: Record<number, string> = {
1: "bg-[linear-gradient(90deg,rgba(23,25,28,1)_0%,rgba(19,20,23,1)_100%)]",
2: "bg-[linear-gradient(90deg,rgba(11,43,24,1)_0%,rgba(7,18,13,1)_100%)]",
3: "bg-[linear-gradient(90deg,rgba(0,160,181,1)_0%,rgba(0,0,0,1)_100%)]",
4: "bg-gradient-to-r from-[#a300b8] to-black",
5: "bg-[linear-gradient(90deg,rgba(138,90,0,1)_0%,rgba(15,8,0,1)_100%)]",
};
const rarityGradients: Record<number, string> = {
1: "linear-gradient(90deg, rgba(23, 25, 28, 1) 0%, rgba(19, 20, 23, 1) 100%)",
2: "linear-gradient(90deg, rgba(11, 43, 24, 1) 0%, rgba(7, 18, 13, 1) 100%)",
3: "linear-gradient(90deg, rgba(0, 160, 181, 1) 0%, rgba(0, 0, 0, 1) 100%)",
4: "linear-gradient(90deg, rgba(163, 0, 184, 1) 0%, rgba(0, 0, 0, 1) 100%)",
5: "linear-gradient(90deg, rgba(138, 90, 0, 1) 0%, rgba(15, 8, 0, 1) 100%)",
};
export function rarityGradientClass(rarity: number | null | undefined): string | undefined {
return rarity ? rarityGradientClasses[rarity] : undefined;
}
export function rarityGradient(rarity: number | null | undefined): string {
return (rarity && rarityGradients[rarity]) || "#17383d";
}
+4 -3
View File
@@ -39,6 +39,7 @@ const weaponSchema = z.record(z.string(), z.object({
const artifactSchema = z.record(z.string(), z.object({
enName: z.string(),
setIcon: z.string(),
qualityType: z.string().optional(),
}));
async function getJson(url: string): Promise<unknown> {
@@ -145,7 +146,7 @@ export async function processCatalogSyncJob(jobId: string): Promise<void> {
sourceImage: `${API_ROOT}/assets/weaponicon/${encodeURIComponent(webpName(value.weaponIcon))}`,
}));
let artifacts = Object.entries(artifactData).filter(([, value]) => value.enName && value.enName !== "???" && value.setIcon).map(([key, value]) => ({
version, key, name: value.enName,
version, key, name: value.enName, rarity: rarity(value.qualityType),
imageKey: `catalog/${version}/artifacts/${webpName(value.setIcon)}`,
sourceImage: `${API_ROOT}/assets/artifacts/${encodeURIComponent(webpName(value.setIcon))}`,
}));
@@ -176,7 +177,7 @@ export async function processCatalogSyncJob(jobId: string): Promise<void> {
await Promise.all([
storage.write(`catalog/${version}/characters.json`, JSON.stringify(characters.map((item) => ({ version: item.version, key: item.key, name: item.name, element: item.element, rarity: item.rarity, weaponType: item.weaponType, imageKey: item.imageKey, elementIconKey: item.elementIconKey, constellations: item.constellations }))), { type: "application/json", acl: "public-read" }),
storage.write(`catalog/${version}/weapons.json`, JSON.stringify(weapons.map((item) => ({ version: item.version, key: item.key, name: item.name, rarity: item.rarity, weaponType: item.weaponType, imageKey: item.imageKey }))), { type: "application/json", acl: "public-read" }),
storage.write(`catalog/${version}/artifacts.json`, JSON.stringify(artifacts.map((item) => ({ version: item.version, key: item.key, name: item.name, imageKey: item.imageKey }))), { type: "application/json", acl: "public-read" }),
storage.write(`catalog/${version}/artifacts.json`, JSON.stringify(artifacts.map((item) => ({ version: item.version, key: item.key, name: item.name, rarity: item.rarity, imageKey: item.imageKey }))), { type: "application/json", acl: "public-read" }),
]);
await getDb().transaction(async (tx) => {
@@ -186,7 +187,7 @@ export async function processCatalogSyncJob(jobId: string): Promise<void> {
await tx.delete(catalogArtifacts).where(eq(catalogArtifacts.version, version));
if (characters.length) await tx.insert(catalogCharacters).values(characters.map((item) => ({ version: item.version, key: item.key, name: item.name, element: item.element, rarity: item.rarity, weaponType: item.weaponType, imageKey: item.imageKey, elementIconKey: item.elementIconKey, constellations: item.constellations })));
if (weapons.length) await tx.insert(catalogWeapons).values(weapons.map((item) => ({ version: item.version, key: item.key, name: item.name, rarity: item.rarity, weaponType: item.weaponType, imageKey: item.imageKey })));
if (artifacts.length) await tx.insert(catalogArtifacts).values(artifacts.map((item) => ({ version: item.version, key: item.key, name: item.name, imageKey: item.imageKey })));
if (artifacts.length) await tx.insert(catalogArtifacts).values(artifacts.map((item) => ({ version: item.version, key: item.key, name: item.name, rarity: item.rarity, imageKey: item.imageKey })));
await tx.update(catalogVersions).set({ active: false });
await tx.update(catalogVersions).set({ active: true, syncedAt: new Date() }).where(eq(catalogVersions.version, version));
await tx.update(catalogSyncJobs).set({ status: "completed", completedAt: new Date(), progress: uniqueAssets.size, total: uniqueAssets.size }).where(eq(catalogSyncJobs.id, jobId));