feat : update
CI / Verify (push) Successful in 1m5s
CI / Build immutable images and deploy (push) Successful in 1m56s

This commit is contained in:
2026-09-03 19:15:10 +07:00 Unverified
parent 381ee216dd
commit 1b014d7c8f
8 changed files with 226 additions and 704 deletions
+1 -16
View File
@@ -1,23 +1,8 @@
import { describe, expect, it } from "vitest";
import {
constellationChartRows,
imageContentDisposition,
} from "@/lib/guides/export-image";
import { constellationChartRows } from "@/lib/guides/export-image";
describe("guide image downloads", () => {
it("provides a safe ASCII fallback and a UTF-8 filename", () => {
expect(imageContentDisposition("ไกด์อาวุธ", "weapons")).toBe(
"attachment; filename=\"weapons.png\"; filename*=UTF-8''%E0%B9%84%E0%B8%81%E0%B8%94%E0%B9%8C%E0%B8%AD%E0%B8%B2%E0%B8%A7%E0%B8%B8%E0%B8%98.png",
);
});
it("sanitizes punctuation in the fallback filename", () => {
expect(imageContentDisposition("Arlecchino / R5", "weapons")).toContain(
'filename="Arlecchino-R5.png"',
);
});
it("orders constellation levels, adds C0, and scales against the maximum", () => {
const rows = constellationChartRows([
{ level: 1, cumulativePercent: "117", isGreat: false },
-13
View File
@@ -1,16 +1,3 @@
export function imageContentDisposition(displayName: string, fallbackName: string): string {
const safeDisplayName = displayName.trim() || fallbackName;
const fallback = safeDisplayName
.normalize("NFKD")
.replace(/[^a-z0-9]+/giu, "-")
.replace(/^-|-$/gu, "") || fallbackName;
const encoded = encodeURIComponent(`${safeDisplayName}.png`).replace(
/['()*]/gu,
(character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`,
);
return `attachment; filename="${fallback}.png"; filename*=UTF-8''${encoded}`;
}
export interface ConstellationChartRow {
level: number;
percent: number;
-53
View File
@@ -142,57 +142,4 @@ export async function getPublicGuide(slug: string) {
};
}
export async function getPublicTeamExport(teamId: string) {
const [row] = await getDb()
.select({ team: teamCompositions, guide: guides })
.from(teamCompositions)
.innerJoin(guides, eq(guides.id, teamCompositions.guideId))
.innerJoin(
guideSections,
and(
eq(guideSections.guideId, guides.id),
eq(guideSections.slug, "team"),
eq(guideSections.enabled, true),
),
)
.where(and(eq(teamCompositions.id, teamId), isNull(guides.trashedAt)))
.limit(1);
if (!row) return null;
const members = await getDb()
.select({ member: teamMembers, character: catalogCharacters })
.from(teamMembers)
.innerJoin(
catalogCharacters,
eq(catalogCharacters.key, teamMembers.characterKey),
)
.where(eq(teamMembers.teamId, teamId))
.orderBy(asc(teamMembers.position));
const weaponKeys = members.map(({ member }) => member.weaponKey).filter((key): key is string => Boolean(key));
const artifactKeys = members.flatMap(({ member }) => [member.artifactKey, member.secondArtifactKey]).filter((key): key is string => Boolean(key));
const [weapons, artifacts] = await Promise.all([
weaponKeys.length ? getDb().select().from(catalogWeapons).where(inArray(catalogWeapons.key, weaponKeys)) : [],
artifactKeys.length ? getDb().select().from(catalogArtifacts).where(inArray(catalogArtifacts.key, artifactKeys)) : [],
]);
return {
...row.team,
guideId: row.guide.id,
guideName: row.guide.name,
members: members.map(({ member, character }) => {
const weapon = weapons.find((item) => item.key === member.weaponKey);
const artifact = artifacts.find((item) => item.key === member.artifactKey);
const secondArtifact = artifacts.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,
};
}),
};
}
export type PublicGuide = NonNullable<Awaited<ReturnType<typeof getPublicGuide>>>;