Files
2026-09-14 18:02:18 +07:00

163 lines
5.8 KiB
TypeScript

"use client";
import { useTranslation } from "@/components/i18n/locale-provider";
import Image from "next/image";
import { DownloadIcon } from "lucide-react";
import { GuideExportBranding } from "@/components/public/guide-export-branding";
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, locale: string): string {
if (value === null) return "-%";
const numeric = Number(value);
if (numeric === 0) return "-%";
return `${numeric.toLocaleString(locale, { maximumFractionDigits: 2 })}%`;
}
function WeaponComparisonImage({
guide,
group,
metric,
conditionIndex,
}: {
guide: PublicGuide;
group: WeaponGroup;
metric: DamageMetric;
conditionIndex: number;
}) {
const { e, locale } = useTranslation();
const conditionLabel = group.name || e("condition", { index: conditionIndex + 1 });
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-36 text-center drop-shadow-[0_4px_3px_rgba(0,0,0,0.9)]">
<h2 className="max-w-full truncate text-4xl leading-tight font-bold">
{e("weaponTitle", { name: guide.character.name })}
</h2>
<p className="mt-1 text-3xl font-bold text-white">
{e(metric === "overallPercent" ? "overallDamage" : "personalDamage")}
</p>
<p className="mt-2 max-w-full truncate text-xl 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 min-w-0 shrink-0 items-center gap-2 rounded-r-xl bg-white px-3 text-[#15121a]"
style={{ width: `${width}%`, maxWidth: "calc(100% - 140px)" }}
>
<span className="min-w-0 flex-1 truncate text-[22px] font-bold">
{row.weapon.name}
</span>
<span className="shrink-0 text-lg font-bold">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 whitespace-nowrap text-[26px]">{formattedPercent(row[metric], locale)}</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 { e } = useTranslation();
const conditionLabel = group.name || e("condition", { index: conditionIndex + 1 });
const baseName = `${guide.name}-Weapon-${conditionLabel}`;
const metricOptions = ([
["overallPercent", e("overallDamage")],
["personalPercent", e("personalDamage")],
] 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" />
{e("exportImage")}
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{e("damageTitle")}</DialogTitle>
<DialogDescription>
{e("damageDescription")}
</DialogDescription>
</DialogHeader>
<div className="grid gap-2 sm:grid-cols-2">
{metricOptions.map(([metric, label]) => (
<GuideImageExport
allowWithoutNote
key={metric}
fileName={`${baseName}-${label}.png`}
width={1200}
captureClassName="p-0"
buttonLabel={label}
>
<WeaponComparisonImage
guide={guide}
group={group}
metric={metric}
conditionIndex={conditionIndex}
/>
</GuideImageExport>
))}
</div>
</DialogContent>
</Dialog>
);
}