163 lines
5.8 KiB
TypeScript
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>
|
|
);
|
|
}
|