Files
buzz-sheet/components/public/weapon-image-export.tsx
T

185 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import Image from "next/image";
import { DownloadIcon } from "lucide-react";
import { useState, type ReactNode } from "react";
import { GuideExportBranding } from "@/components/public/guide-export-branding";
import {
ExportText,
} 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 { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
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 })}%`;
}
export 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>
{conditionLabel ? (
<p className="mt-2 text-2xl font-semibold">{conditionLabel}</p>
) : null}
</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 conditionLabel = group.name;
const baseName = `${guide.name}-Weapon-${conditionLabel || `Condition ${conditionIndex + 1}`}`;
const metricOptions = ([
["overallPercent", "Overall Damage"],
["personalPercent", "Personal Damage"],
] as const).filter(([metric]) =>
group.rows.some((row) => row[metric] !== null),
);
const [metric, setMetric] = useState<DamageMetric>(
metricOptions[0]?.[0] ?? "overallPercent",
);
const metricLabel = metric === "overallPercent" ? "Overall Damage" : "Personal Damage";
return (
<Dialog>
<DialogTrigger render={<Button type="button" variant="outline" size="sm" />}>
<DownloadIcon data-icon="inline-start" />
ส่งออกภาพ
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>เลือกประเภทดาเมจ</DialogTitle>
<DialogDescription>
เลือกค่าที่ต้องการใช้สร้างภาพเปรียบเทียบอาวุธ
</DialogDescription>
</DialogHeader>
<div className="flex flex-col gap-2">
<p className="text-sm font-medium">ประเภทดาเมจ</p>
<ToggleGroup
aria-label="ประเภทดาเมจในภาพส่งออก"
variant="outline"
value={[metric]}
onValueChange={(values) => {
const next = values[0];
if (next === "overallPercent" || next === "personalPercent") setMetric(next);
}}
>
{metricOptions.map(([value]) => (
<ToggleGroupItem key={value} value={value}>
{value === "overallPercent" ? "ดาเมจทั้งทีม" : "ดาเมจส่วนตัว"}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<GuideImageExport
fileName={`${baseName}-${metricLabel}.png`}
width={1200}
captureClassName="p-0"
modeSelection="inline"
>
<WeaponComparisonImage
guide={guide}
group={group}
metric={metric}
conditionLabel={conditionLabel}
/>
</GuideImageExport>
</DialogContent>
</Dialog>
);
}