From 69530932f206daf3d0f6fe3bb17f5446c96119a7 Mon Sep 17 00:00:00 2001 From: gunshiz Date: Tue, 1 Sep 2026 22:36:57 +0700 Subject: [PATCH] feat : update --- .../[guideId]/artifacts/image/route.tsx | 743 ++++++++++++++---- .../guides/[guideId]/weapons/image/route.tsx | 243 ++++-- bun.lock | 3 + components/public/structured-guide.tsx | 2 +- package.json | 1 + 5 files changed, 799 insertions(+), 193 deletions(-) diff --git a/app/api/guides/[guideId]/artifacts/image/route.tsx b/app/api/guides/[guideId]/artifacts/image/route.tsx index b44fe5d..6fcd4b9 100644 --- a/app/api/guides/[guideId]/artifacts/image/route.tsx +++ b/app/api/guides/[guideId]/artifacts/image/route.tsx @@ -8,8 +8,18 @@ import { rarityGradient } from "@/lib/catalog/rarity"; import { artifactTierCount } from "@/lib/guides/artifacts"; import { getCachedPublicArtifactExport } from "@/lib/cache/public"; import { imageContentDisposition } from "@/lib/guides/export-image"; -import { normalizeTargetStat, recommendedStatIconName, statPriorityColor, targetStatIconName, targetStatOptions } from "@/lib/guides/stats"; -import { normalizeTalentPriorities, talentRelationSymbol, talentTypeLabel } from "@/lib/guides/talents"; +import { + normalizeTargetStat, + recommendedStatIconName, + statPriorityColor, + targetStatIconName, + targetStatOptions, +} from "@/lib/guides/stats"; +import { + normalizeTalentPriorities, + talentRelationSymbol, + talentTypeLabel, +} from "@/lib/guides/talents"; const anuphanFonts = Promise.all( [ @@ -21,7 +31,8 @@ const anuphanFonts = Promise.all( const response = await fetch( `https://unpkg.com/@fontsource/anuphan@5.3.0/files/anuphan-${subset}-${weight}-normal.woff`, ); - if (!response.ok) throw new Error(`Unable to load Anuphan ${subset} ${weight}`); + if (!response.ok) + throw new Error(`Unable to load Anuphan ${subset} ${weight}`); return { name: "Anuphan", data: await response.arrayBuffer(), @@ -75,10 +86,16 @@ async function pngDataUrl(url: string): Promise { } } -async function localPngDataUrl(fileName: string, size: number | null = 96, white = false): Promise { +async function localPngDataUrl( + fileName: string, + size: number | null = 96, + white = false, +): Promise { let image = sharp(join(process.cwd(), "public", "icon", fileName)); if (white) image = image.negate({ alpha: false }); - const png = await (size === null ? image : image.resize(size, size, { fit: "contain" })) + const png = await ( + size === null ? image : image.resize(size, size, { fit: "contain" }) + ) .png() .toBuffer(); return `data:image/png;base64,${png.toString("base64")}`; @@ -93,46 +110,88 @@ export async function GET( if (!guide) return new Response("ไม่พบไกด์", { status: 404 }); const talents = normalizeTalentPriorities(guide.profile?.talents ?? []); - const [fonts, groupAssets, statIconAssets, talentAssets, targetStatAssets] = await Promise.all([ - anuphanFonts, - Promise.all( - guide.groups.map((group) => - Promise.all( - group.rows.map(async (row) => ({ - first: await pngDataUrl(row.first.imageUrl), - second: row.second ? await pngDataUrl(row.second.imageUrl) : null, - })), + const [fonts, groupAssets, statIconAssets, talentAssets, targetStatAssets] = + await Promise.all([ + anuphanFonts, + Promise.all( + guide.groups.map((group) => + Promise.all( + group.rows.map(async (row) => ({ + first: await pngDataUrl(row.first.imageUrl), + second: row.second ? await pngDataUrl(row.second.imageUrl) : null, + })), + ), ), ), - ), - Promise.all([ - localPngDataUrl("Icon_Sands_of_Eon.webp"), - localPngDataUrl("Icon_Goblet_of_Eonothem.webp"), - localPngDataUrl("Icon_Circlet_of_Logos.webp"), - ]), - Promise.all(talents.map((talent) => { - const catalogTalent = guide.character.talents.find((item) => item.type === talent.type); - return catalogTalent?.iconUrl ? pngDataUrl(catalogTalent.iconUrl) : null; - })), - Promise.all(targetStatOptions.map((stat) => localPngDataUrl(`stats/${targetStatIconName(stat)}.webp`, 96, true))), - ]); + Promise.all([ + localPngDataUrl("Icon_Sands_of_Eon.webp"), + localPngDataUrl("Icon_Goblet_of_Eonothem.webp"), + localPngDataUrl("Icon_Circlet_of_Logos.webp"), + ]), + Promise.all( + talents.map((talent) => { + const catalogTalent = guide.character.talents.find( + (item) => item.type === talent.type, + ); + return catalogTalent?.iconUrl + ? pngDataUrl(catalogTalent.iconUrl) + : null; + }), + ), + Promise.all( + targetStatOptions.map((stat) => + localPngDataUrl(`stats/${targetStatIconName(stat)}.webp`, 96, true), + ), + ), + ]); const profile = guide.profile; const recommendedStatAsset = (value: string) => { const iconName = recommendedStatIconName(value); - const iconIndex = targetStatOptions.findIndex((stat) => targetStatIconName(stat) === iconName); + const iconIndex = targetStatOptions.findIndex( + (stat) => targetStatIconName(stat) === iconName, + ); return iconIndex >= 0 ? targetStatAssets[iconIndex] : null; }; - const groupRowHeights = guide.groups.map((group) => group.rows.map((row) => Math.max(76, 76 + (row.note ? Math.ceil(row.note.length / 58) * 22 : 0)))); - const tierListHeight = guide.groups.reduce((sum, _group, groupIndex) => sum + 68 + groupRowHeights[groupIndex].reduce((groupSum, rowHeight) => groupSum + rowHeight, 0), 0) + Math.max(0, guide.groups.length - 1) * 16; + const groupRowHeights = guide.groups.map((group) => + group.rows.map((row) => + Math.max(76, 76 + (row.note ? Math.ceil(row.note.length / 58) * 22 : 0)), + ), + ); + const tierListHeight = + guide.groups.reduce( + (sum, _group, groupIndex) => + sum + + 68 + + groupRowHeights[groupIndex].reduce( + (groupSum, rowHeight) => groupSum + rowHeight, + 0, + ), + 0, + ) + + Math.max(0, guide.groups.length - 1) * 16; const detailHeight = profile - ? 60 + profile.targets.length * 40 + (profile.talents.length ? 82 : 0) + (profile.note ? Math.max(48, Math.ceil(profile.note.length / 58) * 24) : 0) + ? 60 + + profile.targets.length * 40 + + (profile.talents.length ? 82 : 0) + + (profile.note + ? Math.max(48, Math.ceil(profile.note.length / 58) * 24) + : 0) : 0; const mainStatExtraHeight = profile - ? Math.max(0, Math.max(profile.sands.length, profile.goblet.length, profile.circlet.length) - 2) * 24 + ? Math.max( + 0, + Math.max( + profile.sands.length, + profile.goblet.length, + profile.circlet.length, + ) - 2, + ) * 24 : 0; const profileHeight = profile - ? 242 + mainStatExtraHeight + (profile.substats.length ? 92 + profile.substats.length * 18 : 0) + ? 242 + + mainStatExtraHeight + + (profile.substats.length ? 92 + profile.substats.length * 18 : 0) : 0; const leftHeight = tierListHeight + (profile ? profileHeight + 16 : 0); const rightHeight = detailHeight > 60 ? detailHeight : 0; @@ -153,142 +212,539 @@ export async function GET( position: "relative", }} > -
-
-
- อาร์ติแฟกต์และสเตตัสที่แนะนำ +
+
+
+ อาร์ติแฟกต์และสแตทที่แนะนำ +
+
+ Artifact & Stats +
-
- อาร์ติแฟกต์และค่าสถานะ -
-
-
-
60 ? 1.2 : 1, flexDirection: "column", gap: 16 }}> - {guide.groups.map((group, groupIndex) => ( +
+
60 ? 1.2 : 1, + flexDirection: "column", + gap: 16, + }} + > + {guide.groups.map((group, groupIndex) => ( +
+
+ {group.name === "Late Game" ? "เลทเกม" : group.name} +
+
+ {group.rows.map((row, rowIndex) => ( +
+
+ {groupAssets[groupIndex][rowIndex].first ? ( +
+ +
+ ) : null} + {row.second && + groupAssets[groupIndex][rowIndex].second ? ( +
+ +
+ ) : null} +
+
+
+ {artifactTierCount(row.second?.name ?? null)}{" "} + {row.first.name} + {row.second ? ` + ${row.second.name}` : ""} +
+ {row.note ? ( +
+ {row.note} +
+ ) : null} +
+
+ {Number(row.percent) === 0 ? "-%" : metric(row.percent)} +
+
+ ))} +
+
+ ))} + + {profile ? ( +
+
+ ออฟหลักที่ดีที่สุด +
+
+ {[ + { + label: "นาฬิกา", + values: profile.sands, + icon: statIconAssets[0], + }, + { + label: "ถ้วย", + values: profile.goblet, + icon: statIconAssets[1], + }, + { + label: "หัว", + values: profile.circlet, + icon: statIconAssets[2], + }, + ].map((stat) => ( +
+ +
+ {stat.label} +
+
+
+ {stat.values.length + ? stat.values.map((value, index) => ( + + {recommendedStatAsset(value) ? ( + + ) : null} + {statLabel(value)} + + )) + : "-"} +
+
+ ))} +
+
+ ) : null} + + {profile?.substats.length ? ( +
+ ออฟรองที่ดีที่สุด +
+ {profile.substats.map((stat, index) => ( + + {recommendedStatAsset(stat) ? ( + + ) : null} + {statLabel(stat)} + + ))} +
+
+ ) : null} +
+ + {profile && detailHeight > 60 ? (
-
- {group.name === "Late Game" ? "เลทเกม" : group.name} -
-
- {group.rows.map((row, rowIndex) => ( -
-
- {groupAssets[groupIndex][rowIndex].first ? ( -
- -
- ) : null} - {row.second && groupAssets[groupIndex][rowIndex].second ? ( -
- -
- ) : null} -
-
-
- {artifactTierCount(row.second?.name ?? null)}{" "} - {row.first.name}{row.second ? ` + ${row.second.name}` : ""} -
- {row.note ?
{row.note}
: null} -
-
- {Number(row.percent) === 0 ? "-%" : metric(row.percent)} -
-
- ))} -
-
- ))} - - {profile ? ( -
-
- ออฟหลัก -
-
- {[ - { label: "นาฬิกา", values: profile.sands, icon: statIconAssets[0] }, - { label: "ถ้วย", values: profile.goblet, icon: statIconAssets[1] }, - { label: "หัว", values: profile.circlet, icon: statIconAssets[2] }, - ].map((stat) => ( -
- -
{stat.label}
-
-
- {stat.values.length ? stat.values.map((value, index) => {recommendedStatAsset(value) ? : null}{statLabel(value)}) : "-"} -
-
- ))} -
-
- ) : null} - - {profile?.substats.length ? ( -
- ออฟรอง -
- {profile.substats.map((stat, index) => {recommendedStatAsset(stat) ? : null}{statLabel(stat)})} -
-
- ) : null} -
- - {profile && detailHeight > 60 ? ( -
-
-
รายละเอียดเพิ่มเติม
+
{profile.targets.length ? ( -
-
ค่าสถานะเป้าหมาย
+
+
+ ค่าสถานะต่ำๆที่ควรจะปั้นไว้ +
{profile.targets.map((target, index) => { - const iconIndex = targetStatOptions.findIndex((stat) => stat === normalizeTargetStat(target.stat)); + const iconIndex = targetStatOptions.findIndex( + (stat) => stat === normalizeTargetStat(target.stat), + ); return ( -
- {iconIndex >= 0 ? : null}{normalizeTargetStat(target.stat) ?? target.stat}{target.value} -
+
+ + {iconIndex >= 0 ? ( + + ) : null} + {normalizeTargetStat(target.stat) ?? target.stat} + + {target.value} +
); })}
) : null} {talents.length ? ( -
-
ลำดับการอัพพรสวรรค์
-
+
+
+ ลำดับการอัพพรสวรรค์ +
+
{talents.map((talent, index) => ( -
- {talentAssets[index] ? : null} - - {talent.type}{talent.level ? ` เลเวล ${talent.level}+` : ""} - {talentTypeLabel(talent.type)} +
+ {talentAssets[index] ? ( + + ) : null} + + + {talent.type} + {talent.level ? ` เลเวล ${talent.level}+` : ""} + + + {talentTypeLabel(talent.type)} + - {index < talents.length - 1 ? {talentRelationSymbol(talent.relationToNext)} : null} + {index < talents.length - 1 ? ( + + {talentRelationSymbol(talent.relationToNext)} + + ) : null}
))}
) : null} {profile.note ? ( -
-
โน็ต
-
{profile.note}
+
+
+ โน็ต +
+
+ {profile.note} +
) : null} +
-
- ) : null} -
+ ) : null} +
, { @@ -297,7 +753,10 @@ export async function GET( fonts: fonts.length ? fonts : undefined, headers: { "Cache-Control": "no-store", - "Content-Disposition": imageContentDisposition(`${guide.guideName} อาร์ติแฟกต์`, "artifacts"), + "Content-Disposition": imageContentDisposition( + `${guide.guideName}-Artifact`, + "artifacts", + ), "Content-Type": "image/png", }, }, diff --git a/app/api/guides/[guideId]/weapons/image/route.tsx b/app/api/guides/[guideId]/weapons/image/route.tsx index 78287df..c8b6f37 100644 --- a/app/api/guides/[guideId]/weapons/image/route.tsx +++ b/app/api/guides/[guideId]/weapons/image/route.tsx @@ -16,7 +16,8 @@ const anuphanFonts = Promise.all( const response = await fetch( `https://unpkg.com/@fontsource/anuphan@5.3.0/files/anuphan-${subset}-${weight}-normal.woff`, ); - if (!response.ok) throw new Error(`Unable to load Anuphan ${subset} ${weight}`); + if (!response.ok) + throw new Error(`Unable to load Anuphan ${subset} ${weight}`); return { name: "Anuphan", data: await response.arrayBuffer(), @@ -58,9 +59,19 @@ export async function GET( anuphanFonts, Promise.all(guide.weapons.map((row) => pngDataUrl(row.weapon.imageUrl))), ]); - const weaponHeights = guide.weapons.map((row) => Math.max(126, 126 + (row.note ? Math.ceil(row.note.length / 52) * 22 : 0))); - const noteHeight = guide.note ? Math.max(24, Math.ceil(guide.note.length / 70) * 22) : 0; - const height = Math.max(360, 190 + noteHeight + weaponHeights.reduce((sum, rowHeight) => sum + rowHeight, 0) + Math.max(0, guide.weapons.length - 1) * 14); + const weaponHeights = guide.weapons.map((row) => + Math.max(126, 126 + (row.note ? Math.ceil(row.note.length / 52) * 22 : 0)), + ); + const noteHeight = guide.note + ? Math.max(24, Math.ceil(guide.note.length / 70) * 22) + : 0; + const height = Math.max( + 360, + 190 + + noteHeight + + weaponHeights.reduce((sum, rowHeight) => sum + rowHeight, 0) + + Math.max(0, guide.weapons.length - 1) * 14, + ); return new ImageResponse(
-
-
-
อาวุธที่แนะนำ
-
อาวุธ
- {guide.note ?
{guide.note}
: null} -
- {guide.weapons.map((row, index) => ( +
-
- {weaponImages[index] ? ( - - ) : ( -
{index + 1}
- )} -
-
- {index + 1}. {row.weapon.name} R{row.refinement} + อาวุธที่แนะนำ +
+
+ Weapons +
+ {guide.note ? ( +
+ {guide.note} +
+ ) : null} +
+ {guide.weapons.map((row, index) => ( +
+
+
+ {weaponImages[index] ? ( + + ) : ( +
+ {index + 1} +
+ )} +
+
+ {index + 1}. {row.weapon.name} R{row.refinement} +
+ {row.note ? ( +
+ {row.note} +
+ ) : null}
- {row.note ?
{row.note}
: null}
+
+ {row.overallPercent ? ( +
+
+ {metric(row.overallPercent)} +
+
+ ดาเมททั้งทีม +
+
+ ) : null} + {row.personalPercent ? ( +
+
+ {metric(row.personalPercent)} +
+
+ ดาเมจส่วนตัว +
+
+ ) : null} +
-
- {row.overallPercent ? ( -
-
{metric(row.overallPercent)}
-
ดาเมททั้งทีม
-
- ) : null} - {row.personalPercent ? ( -
-
{metric(row.personalPercent)}
-
ดาเมจส่วนตัว
-
- ) : null} -
-
- ))} + ))}
, { @@ -144,7 +284,10 @@ export async function GET( fonts: fonts.length ? fonts : undefined, headers: { "Cache-Control": "no-store", - "Content-Disposition": imageContentDisposition(`${guide.guideName} อาวุธ`, "weapons"), + "Content-Disposition": imageContentDisposition( + `${guide.guideName}-Weapon`, + "weapons", + ), "Content-Type": "image/png", }, }, diff --git a/bun.lock b/bun.lock index dc8cab3..740f851 100644 --- a/bun.lock +++ b/bun.lock @@ -18,6 +18,7 @@ "embla-carousel-react": "^8.6.0", "ioredis": "^6.0.0", "lucide-react": "^1.35.0", + "modern-screenshot": "^4.7.0", "next": "16.3.3", "postgres": "^3.4.9", "react": "19.2.8", @@ -1231,6 +1232,8 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "modern-screenshot": ["modern-screenshot@4.7.0", "", {}, "sha512-9YxN+ddPSMMlhylOv25VHzXrl9u67QRxoh7+SEewGtgUw7t6hHTrjptSDJUSne9oG4Xk/h2cwG15nIt4Hc9ujg=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], diff --git a/components/public/structured-guide.tsx b/components/public/structured-guide.tsx index 3a9de0c..841dcaa 100644 --- a/components/public/structured-guide.tsx +++ b/components/public/structured-guide.tsx @@ -270,7 +270,7 @@ function WeaponSection({ guide }: { guide: PublicGuide }) {

อาวุธที่แนะนำ

- อาวุธ + Weapons

{note ? (

diff --git a/package.json b/package.json index ba81f37..df49266 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "embla-carousel-react": "^8.6.0", "ioredis": "^6.0.0", "lucide-react": "^1.35.0", + "modern-screenshot": "^4.7.0", "next": "16.3.3", "postgres": "^3.4.9", "react": "19.2.8",