176 lines
6.3 KiB
TypeScript
176 lines
6.3 KiB
TypeScript
"use client";
|
||
|
||
import Image from "next/image";
|
||
import { DownloadIcon } from "lucide-react";
|
||
import type { ReactNode } from "react";
|
||
|
||
import { GuideExportBranding } from "@/components/public/guide-export-branding";
|
||
import {
|
||
ExportLanguageToggle,
|
||
ExportText,
|
||
useExportLanguage,
|
||
} from "@/components/public/export-language";
|
||
import { GuideImageExport } from "@/components/public/guide-image-export";
|
||
import { Button } from "@/components/ui/button";
|
||
import {
|
||
Dialog,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
DialogTrigger,
|
||
} from "@/components/ui/dialog";
|
||
import type { PublicGuide } from "@/lib/guides/queries";
|
||
|
||
type WeaponGroup = PublicGuide["weaponGroups"][number];
|
||
type DamageMetric = "overallPercent" | "personalPercent";
|
||
|
||
function formattedPercent(value: string | null): string {
|
||
if (value === null) return "-%";
|
||
const numeric = Number(value);
|
||
if (numeric === 0) return "-%";
|
||
return `${numeric.toLocaleString("en-US", { maximumFractionDigits: 2 })}%`;
|
||
}
|
||
|
||
function WeaponComparisonImage({
|
||
guide,
|
||
group,
|
||
metric,
|
||
conditionLabel,
|
||
}: {
|
||
guide: PublicGuide;
|
||
group: WeaponGroup;
|
||
metric: DamageMetric;
|
||
conditionLabel: ReactNode;
|
||
}) {
|
||
const values = group.rows.map((row) => Number(row[metric]) || 0);
|
||
const maximum = Math.max(100, ...values);
|
||
|
||
return (
|
||
<div className="relative isolate w-300 overflow-hidden bg-[#070b20] px-14 pt-10 pb-12 font-heading text-white">
|
||
<GuideExportBranding backgroundUrl={guide.character.splashImageUrl ?? guide.coverUrl} />
|
||
<header className="relative z-10 flex flex-col items-center px-48 text-center drop-shadow-[0_4px_3px_rgba(0,0,0,0.9)]">
|
||
<h2 className="max-w-full text-5xl leading-tight font-bold text-balance">
|
||
{`${guide.character.name}’s Weapon Comparison`}
|
||
</h2>
|
||
<p className="mt-1 text-4xl font-bold text-white">
|
||
{metric === "overallPercent" ? (
|
||
<ExportText th="ดาเมจทั้งทีม" en="Overall Damage" />
|
||
) : (
|
||
<ExportText th="ดาเมจส่วนตัว" en="Personal Damage" />
|
||
)}
|
||
</p>
|
||
<p className="mt-2 text-2xl font-semibold">{conditionLabel}</p>
|
||
</header>
|
||
|
||
<div className="relative z-10 mt-9 flex flex-col gap-3">
|
||
{group.rows.map((row) => {
|
||
const numeric = Number(row[metric]) || 0;
|
||
const width = row[metric] === null
|
||
? 28
|
||
: numeric <= 100
|
||
? Math.max(28, Math.max(0, numeric) * 0.5)
|
||
: Math.min(90, 50 + ((numeric - 100) / Math.max(1, maximum - 100)) * 40);
|
||
return (
|
||
<div key={row.id} className="grid min-h-22 grid-cols-[76px_1fr] items-center gap-3">
|
||
<div className="relative size-18">
|
||
<Image
|
||
src={row.weapon.imageUrl}
|
||
alt=""
|
||
fill
|
||
loading="eager"
|
||
sizes="72px"
|
||
className="object-contain drop-shadow-[0_4px_3px_rgba(0,0,0,0.8)]"
|
||
/>
|
||
</div>
|
||
<div className="flex min-w-0 items-center">
|
||
<div
|
||
className="flex min-h-16 shrink-0 items-center rounded-r-xl bg-white px-5 text-[#15121a]"
|
||
style={{ width: `${width}%` }}
|
||
>
|
||
<span className="truncate text-[28px] font-bold">
|
||
{row.weapon.name} R{row.refinement}
|
||
</span>
|
||
</div>
|
||
<div className="ml-3 flex min-w-0 flex-1 flex-col items-start gap-1 text-white drop-shadow-[0_4px_3px_rgba(0,0,0,0.9)]">
|
||
<strong className="shrink-0 text-[30px]">{formattedPercent(row[metric])}</strong>
|
||
{row.note ? <span data-export-note className="min-w-0 text-xl leading-tight">{row.note}</span> : null}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
{group.note ? (
|
||
<p data-export-note className="relative z-10 mt-8 whitespace-pre-wrap text-center text-2xl font-semibold drop-shadow-[0_4px_3px_rgba(0,0,0,0.9)]">
|
||
{group.note}
|
||
</p>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function WeaponImageExport({
|
||
guide,
|
||
group,
|
||
conditionIndex,
|
||
}: {
|
||
guide: PublicGuide;
|
||
group: WeaponGroup;
|
||
conditionIndex: number;
|
||
}) {
|
||
const [language, setLanguage] = useExportLanguage();
|
||
const conditionLabel = group.name || `Condition ${conditionIndex + 1}`;
|
||
const baseName = `${guide.name}-Weapon-${conditionLabel}`;
|
||
const metricOptions = ([
|
||
["overallPercent", "Overall Damage"],
|
||
["personalPercent", "Personal Damage"],
|
||
] as const).filter(([metric]) =>
|
||
group.rows.some((row) => row[metric] !== null),
|
||
);
|
||
|
||
return (
|
||
<Dialog>
|
||
<DialogTrigger render={<Button type="button" variant="outline" size="sm" />}>
|
||
<DownloadIcon data-icon="inline-start" />
|
||
ส่งออกภาพ
|
||
</DialogTrigger>
|
||
<DialogContent>
|
||
<DialogHeader>
|
||
<DialogTitle>เลือกประเภทดาเมจ</DialogTitle>
|
||
<DialogDescription>
|
||
เลือกค่าที่ต้องการใช้สร้างภาพเปรียบเทียบอาวุธ
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<ExportLanguageToggle language={language} onChange={setLanguage} />
|
||
<div className="grid gap-2 sm:grid-cols-2">
|
||
{metricOptions.map(([metric, label]) => (
|
||
<GuideImageExport
|
||
key={metric}
|
||
fileName={`${baseName}-${label}.png`}
|
||
width={1200}
|
||
captureClassName="p-0"
|
||
language={language}
|
||
showLanguageSelector={false}
|
||
buttonLabel={metric === "overallPercent" ? "ดาเมจทั้งทีม" : "ดาเมจส่วนตัว"}
|
||
>
|
||
<WeaponComparisonImage
|
||
guide={guide}
|
||
group={group}
|
||
metric={metric}
|
||
conditionLabel={group.name ? conditionLabel : (
|
||
<ExportText
|
||
th={`เงื่อนไข ${conditionIndex + 1}`}
|
||
en={`Condition ${conditionIndex + 1}`}
|
||
/>
|
||
)}
|
||
/>
|
||
</GuideImageExport>
|
||
))}
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
);
|
||
}
|