feat : update export image
CI / Build immutable images and deploy (push) Has been cancelled
CI / Verify (push) Has been cancelled

This commit is contained in:
2026-09-05 00:42:16 +07:00 Unverified
parent c8d6c44ba5
commit d47c9cb48b
6 changed files with 215 additions and 113 deletions
+209 -63
View File
@@ -21,7 +21,6 @@ import { GuideImageExport } from "@/components/public/guide-image-export";
import { ConstellationImage } from "@/components/public/constellation-image";
import { RichTextContent } from "@/components/rich-text-content";
import { LiveRefresh } from "@/components/live-refresh";
import { GuideTopicIndicator } from "@/components/guide-topic-indicator";
import { rarityGradientClass, rarityTextClass } from "@/lib/catalog/rarity";
import { elementIconFilter } from "@/lib/catalog/element";
import type { PublicGuide } from "@/lib/guides/queries";
@@ -89,7 +88,7 @@ export function StructuredGuideShell({
<div className="flex min-h-svh flex-col bg-background">
<LiveRefresh endpoint={`/api/events/page/${guide.id}`} />
<SiteHeader editHref={`/admin/${guide.characterKey}`} />
<div className="sticky top-0 z-10 bg-background/95 supports-backdrop-filter:backdrop-blur">
<div className="sticky top-0 z-40 bg-background/95 supports-backdrop-filter:backdrop-blur">
<div className="mx-auto flex min-h-16 w-full max-w-7xl items-center gap-3 px-4 sm:px-6">
<Link
href="/"
@@ -127,14 +126,13 @@ export function StructuredGuideShell({
<Link
href={`/${guide.slug}`}
className={cn(
"relative isolate flex min-h-8 shrink-0 items-center gap-2 whitespace-nowrap rounded-md px-3 text-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring",
"flex min-h-8 shrink-0 items-center gap-2 whitespace-nowrap rounded-md px-3 text-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring",
!section
? "text-foreground"
? "bg-muted text-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
prefetch
>
{!section ? <GuideTopicIndicator guideId={guide.id} surface="public" /> : null}
<Image
src={navigationIconPath("overview")!}
alt=""
@@ -149,14 +147,13 @@ export function StructuredGuideShell({
key={item.id}
href={`/${guide.slug}/${item.slug}`}
className={cn(
"relative isolate flex min-h-8 shrink-0 items-center gap-2 whitespace-nowrap rounded-md px-3 text-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring",
"flex min-h-8 shrink-0 items-center gap-2 whitespace-nowrap rounded-md px-3 text-sm transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring",
section === item.slug
? "text-foreground"
? "bg-muted text-foreground"
: "text-muted-foreground hover:bg-muted hover:text-foreground",
)}
prefetch
>
{section === item.slug ? <GuideTopicIndicator guideId={guide.id} surface="public" /> : null}
{navigationIconPath(item.slug) ? (
<Image
src={navigationIconPath(item.slug)!}
@@ -316,65 +313,135 @@ function metric(value: string | null) {
}
function WeaponSection({ guide }: { guide: PublicGuide }) {
return (
<WeaponSectionContent
guide={guide}
action={
<GuideImageExport fileName={`${guide.name}-Weapon.png`}>
<WeaponSectionContent guide={guide} exporting />
</GuideImageExport>
}
/>
);
return <WeaponSectionContent guide={guide} showExportActions />;
}
function WeaponSectionContent({
guide,
action,
exporting = false,
groups: providedGroups,
conditionNumber,
showExportActions = false,
}: {
guide: PublicGuide;
action?: React.ReactNode;
exporting?: boolean;
groups?: PublicGuide["weaponGroups"];
conditionNumber?: number;
showExportActions?: boolean;
}) {
const note = guide.sections.find(
(section) => section.slug === "weapon",
)?.note;
const weaponGroups = (providedGroups ?? guide.weaponGroups).filter(
(group) => group.rows.length > 0,
);
const useCompactExportGrid =
exporting &&
weaponGroups.reduce((total, group) => total + group.rows.length, 0) > 6;
return (
<section className="flex flex-col gap-6">
<header className="flex flex-col items-center gap-3 text-center">
<p className="text-sm font-medium text-primary">อาวุธที่แนะนำ</p>
<h1 className="whitespace-nowrap font-heading text-4xl font-semibold">
Weapons
</h1>
{note ? (
<p className="max-w-3xl whitespace-pre-wrap text-muted-foreground">
{note}
</p>
<section
className={cn(
"flex flex-col gap-6",
exporting &&
"relative isolate overflow-hidden bg-[#070b20] p-10 text-white",
)}
>
{exporting ? (
<>
<Image
src={guide.character.splashImageUrl ?? guide.coverUrl}
alt=""
fill
loading="eager"
sizes="1200px"
className="object-cover"
/>
<div className="absolute inset-0 bg-[#050817]/65" />
<Image
src="/icon/Watermark.png"
alt=""
width={180}
height={101}
loading="eager"
className="absolute top-4 right-4 z-30 h-auto w-[180px] object-contain drop-shadow-[0_4px_3px_rgba(0,0,0,0.8)]"
/>
</>
) : null}
<header
className={cn(
"relative z-10 flex items-center justify-center gap-3 text-center",
!exporting && "flex-col",
)}
>
{exporting ? (
<Image
src="/icon/nav/Weapon.png"
alt=""
width={56}
height={56}
loading="eager"
className="size-14 shrink-0 object-contain drop-shadow-[0_4px_3px_rgba(0,0,0,0.8)]"
/>
) : null}
{action}
<div className="flex flex-col items-center gap-3">
<p className="text-sm font-medium text-primary">อาวุธที่แนะนำ</p>
<h1 className="whitespace-nowrap font-heading text-4xl font-semibold">
Weapons
</h1>
{note ? (
<p className="max-w-3xl whitespace-pre-wrap text-muted-foreground">
{note}
</p>
) : null}
</div>
</header>
{guide.weaponGroups.filter((group) => group.rows.length > 0).map((group, groupIndex, groups) => (
<div key={group.id} className="flex flex-col gap-3">
{groups.length > 1 ? <h2 className="font-heading text-2xl font-semibold">Condition {groupIndex + 1}</h2> : null}
{weaponGroups.map((group, groupIndex, groups) => (
<div key={group.id} className="relative z-10 flex flex-col gap-3">
<div className="flex items-center justify-between gap-3">
{groups.length > 1 || conditionNumber ? (
<h2 className="font-heading text-2xl font-semibold">
Condition {conditionNumber ?? groupIndex + 1}
</h2>
) : <span />}
{showExportActions ? (
<GuideImageExport
fileName={`${guide.name}-Weapon-Condition-${groupIndex + 1}.png`}
captureClassName="p-0"
>
<WeaponSectionContent
guide={guide}
exporting
groups={[group]}
conditionNumber={groups.length > 1 ? groupIndex + 1 : undefined}
/>
</GuideImageExport>
) : null}
</div>
{group.note ? <p className="whitespace-pre-wrap text-muted-foreground">{group.note}</p> : null}
<div className="grid gap-3">
<div
className={cn("grid gap-3", useCompactExportGrid && "grid-cols-2")}
>
{group.rows.map((row, index) => (
<Card key={row.id} size="sm" className="bg-white py-0">
<CardContent
className={cn(
"grid p-0",
exporting
? "grid-cols-[minmax(0,3fr)_minmax(0,1fr)]"
: "md:grid-cols-[minmax(0,3fr)_minmax(0,1fr)]",
useCompactExportGrid
? "grid-cols-[minmax(0,3fr)_minmax(0,2fr)]"
: exporting
? "grid-cols-[minmax(0,3fr)_minmax(0,1fr)]"
: "md:grid-cols-[minmax(0,3fr)_minmax(0,1fr)]",
)}
>
<div
className={cn(
"flex min-h-28 min-w-0 items-center gap-4 rounded-br-[4rem] p-4 pr-10 text-white",
exporting
? "rounded-br-[8rem] pr-16"
: "md:rounded-br-[8rem] md:pr-16",
useCompactExportGrid
? "rounded-br-[8rem] pr-10"
: exporting
? "rounded-br-[8rem] pr-16"
: "md:rounded-br-[8rem] md:pr-16",
rarityGradientClass(row.weapon.rarity),
)}
>
@@ -392,23 +459,43 @@ function WeaponSectionContent({
<p
className={cn(
"truncate font-semibold",
exporting ? "text-lg" : "text-base sm:text-lg",
useCompactExportGrid
? "text-sm"
: exporting
? "text-lg"
: "text-base sm:text-lg",
)}
>
{index + 1}. {row.weapon.name} R{row.refinement}
</p>
{row.note ? (
<p className="whitespace-pre-wrap text-sm">{row.note}</p>
<p
className={cn(
"whitespace-pre-wrap",
useCompactExportGrid ? "text-xs" : "text-sm",
)}
>
{row.note}
</p>
) : null}
</div>
</div>
<div className="flex items-center justify-around gap-4 bg-white px-4 py-5 text-black">
<div
className={cn(
"flex items-center justify-around bg-white py-5 text-black",
useCompactExportGrid ? "gap-1 px-2" : "gap-4 px-4",
)}
>
{row.overallPercent ? (
<div className="flex-1 text-center">
<div className="min-w-0 flex-1 text-center">
<p
className={cn(
"whitespace-nowrap leading-none font-bold",
exporting ? "text-5xl" : "text-4xl sm:text-5xl",
useCompactExportGrid
? "text-2xl"
: exporting
? "text-5xl"
: "text-4xl sm:text-5xl",
rarityTextClass(row.weapon.rarity),
)}
>
@@ -418,8 +505,12 @@ function WeaponSectionContent({
</p>
<p
className={cn(
"mt-1",
exporting ? "text-base" : "text-sm sm:text-base",
"mt-1 whitespace-nowrap",
useCompactExportGrid
? "text-[10px]"
: exporting
? "text-base"
: "text-sm sm:text-base",
)}
>
ดาเมททั้งทีม
@@ -427,11 +518,15 @@ function WeaponSectionContent({
</div>
) : null}
{row.personalPercent ? (
<div className="flex-1 text-center">
<div className="min-w-0 flex-1 text-center">
<p
className={cn(
"whitespace-nowrap leading-none font-bold",
exporting ? "text-5xl" : "text-4xl sm:text-5xl",
useCompactExportGrid
? "text-2xl"
: exporting
? "text-5xl"
: "text-4xl sm:text-5xl",
rarityTextClass(row.weapon.rarity),
)}
>
@@ -441,8 +536,12 @@ function WeaponSectionContent({
</p>
<p
className={cn(
"mt-1",
exporting ? "text-base" : "text-sm sm:text-base",
"mt-1 whitespace-nowrap",
useCompactExportGrid
? "text-[10px]"
: exporting
? "text-base"
: "text-sm sm:text-base",
)}
>
ดาเมจส่วนตัว
@@ -465,7 +564,10 @@ function ArtifactSection({ guide }: { guide: PublicGuide }) {
<ArtifactSectionContent
guide={guide}
action={
<GuideImageExport fileName={`${guide.name}-Artifact.png`}>
<GuideImageExport
fileName={`${guide.name}-Artifact.png`}
captureClassName="p-0"
>
<ArtifactSectionContent guide={guide} exporting />
</GuideImageExport>
}
@@ -484,19 +586,63 @@ function ArtifactSectionContent({
}) {
const profile = guide.artifactProfile;
return (
<section className="flex flex-col gap-6">
<header className="flex flex-col items-center gap-3 text-center">
<p className="text-sm font-medium text-primary">
อาร์ติแฟกต์และสแตทที่แนะนำ
</p>
<h1 className="font-heading text-4xl font-semibold">
Artifact & Stats
</h1>
{action}
<section
className={cn(
"flex flex-col gap-6",
exporting &&
"relative isolate overflow-hidden bg-[#070b20] p-10 text-white",
)}
>
{exporting ? (
<>
<Image
src={guide.character.splashImageUrl ?? guide.coverUrl}
alt=""
fill
loading="eager"
sizes="1200px"
className="object-cover"
/>
<div className="absolute inset-0 bg-[#050817]/65" />
<Image
src="/icon/Watermark.png"
alt=""
width={180}
height={101}
loading="eager"
className="absolute top-4 right-4 z-30 h-auto w-[180px] object-contain drop-shadow-[0_4px_3px_rgba(0,0,0,0.8)]"
/>
</>
) : null}
<header
className={cn(
"relative z-10 flex items-center justify-center gap-3 text-center",
!exporting && "flex-col",
)}
>
{exporting ? (
<Image
src="/icon/nav/Artifact.webp"
alt=""
width={56}
height={56}
loading="eager"
className="size-14 shrink-0 object-contain drop-shadow-[0_4px_3px_rgba(0,0,0,0.8)]"
/>
) : null}
<div className="flex flex-col items-center gap-3">
<p className="text-sm font-medium text-primary">
อาร์ติแฟกต์และสแตทที่แนะนำ
</p>
<h1 className="font-heading text-4xl font-semibold">
Artifact & Stats
</h1>
{action}
</div>
</header>
<div
className={cn(
"grid gap-5",
"relative z-10 grid gap-5",
exporting ? "grid-cols-[1.2fr_1fr]" : "lg:grid-cols-[1.2fr_1fr]",
)}
>