feat : add language export and some image gen optimaize
This commit is contained in:
@@ -80,6 +80,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Slider } from "@/components/ui/slider";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
import { elementIconFilter } from "@/lib/catalog/element";
|
||||
@@ -121,6 +122,7 @@ type Composition = {
|
||||
fadeColor: string;
|
||||
fadeTint: number;
|
||||
auraUpOffset: number;
|
||||
characterScale: number;
|
||||
characterOffset: Position;
|
||||
fadeOffset: Position;
|
||||
};
|
||||
@@ -140,10 +142,12 @@ const blendModeItems: Array<{ value: OverviewBlendMode; label: string }> = [
|
||||
];
|
||||
|
||||
const DISCLAIMER = "ไม่สามารถเอาไปเปรียบเทียบกับการคำนวณของ TC ท่านอื่นๆได้\nเพราะแต่ละคนใช้สแตทในการคำนวณที่ไม่เหมือนกัน\nและตารางที่เห็นอาจมีการเปลี่ยนแปลงได้ในอนาคต";
|
||||
const DEFAULT_FADE_OFFSET: Position = { x: 0, y: 2 };
|
||||
|
||||
function DraggablePosterLayer({
|
||||
id,
|
||||
offset,
|
||||
scale = 1,
|
||||
interactive,
|
||||
className,
|
||||
style,
|
||||
@@ -151,6 +155,7 @@ function DraggablePosterLayer({
|
||||
}: {
|
||||
id: PosterLayer;
|
||||
offset: Position;
|
||||
scale?: number;
|
||||
interactive: boolean;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
@@ -171,7 +176,7 @@ function DraggablePosterLayer({
|
||||
className={className}
|
||||
style={{
|
||||
...style,
|
||||
transform: `translate(${offset.x}%, ${offset.y}%)${temporaryTransform}`,
|
||||
transform: `translate(${offset.x}%, ${offset.y}%)${temporaryTransform} scale(${scale})`,
|
||||
}}
|
||||
{...dragProps}
|
||||
>
|
||||
@@ -193,7 +198,6 @@ function OverviewPoster({
|
||||
nameCardSize,
|
||||
elementIconUrls,
|
||||
composition,
|
||||
disclaimerFontSize,
|
||||
interactive = false,
|
||||
activeLayer,
|
||||
onLayerMove,
|
||||
@@ -210,7 +214,6 @@ function OverviewPoster({
|
||||
nameCardSize: { width: number; height: number } | null;
|
||||
elementIconUrls: Record<string, string | null>;
|
||||
composition: Composition;
|
||||
disclaimerFontSize: number | null;
|
||||
interactive?: boolean;
|
||||
activeLayer?: PosterLayer;
|
||||
onLayerMove?: (layer: PosterLayer, delta: Position) => void;
|
||||
@@ -224,7 +227,6 @@ function OverviewPoster({
|
||||
text: 5,
|
||||
} as const;
|
||||
const titleRef = useRef<HTMLParagraphElement>(null);
|
||||
const disclaimerRef = useRef<HTMLParagraphElement>(null);
|
||||
const posterRef = useRef<HTMLDivElement>(null);
|
||||
const posterSensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
||||
@@ -243,29 +245,13 @@ function OverviewPoster({
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const fit = () => {
|
||||
if (cancelled || !titleRef.current || !disclaimerRef.current) return;
|
||||
if (cancelled || !titleRef.current) return;
|
||||
const title = titleRef.current;
|
||||
const disclaimer = disclaimerRef.current;
|
||||
const width = title.parentElement!.getBoundingClientRect().width;
|
||||
title.style.fontSize = `${width / 10}px`;
|
||||
if (title.scrollWidth > width) {
|
||||
title.style.fontSize = `${((width / 10) * width) / title.scrollWidth}px`;
|
||||
}
|
||||
const textWidth = title.getBoundingClientRect().width;
|
||||
const context = document.createElement("canvas").getContext("2d");
|
||||
if (!context) return;
|
||||
context.font = '100px "Genshin Overview"';
|
||||
const longest = Math.max(
|
||||
...DISCLAIMER.split("\n").map(
|
||||
(line) => context.measureText(line).width,
|
||||
),
|
||||
);
|
||||
disclaimer.style.width = "max-content";
|
||||
disclaimer.style.textAlign = "center";
|
||||
disclaimer.style.fontSize =
|
||||
disclaimerFontSize === null
|
||||
? `${Math.min(width * 0.024, (textWidth * 100) / longest)}px`
|
||||
: `${disclaimerFontSize}cqw`;
|
||||
};
|
||||
const observer = new ResizeObserver(fit);
|
||||
if (titleRef.current?.parentElement)
|
||||
@@ -275,7 +261,7 @@ function OverviewPoster({
|
||||
cancelled = true;
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [character?.name, disclaimerFontSize]);
|
||||
}, [character?.name]);
|
||||
|
||||
return (
|
||||
<DndContext sensors={posterSensors} onDragEnd={handlePosterDragEnd}>
|
||||
@@ -326,6 +312,7 @@ function OverviewPoster({
|
||||
<DraggablePosterLayer
|
||||
id="character"
|
||||
offset={composition.characterOffset}
|
||||
scale={composition.characterScale}
|
||||
interactive={interactive && activeLayer === "character"}
|
||||
className={cn(
|
||||
"absolute inset-0 cursor-grab active:cursor-grabbing",
|
||||
@@ -340,7 +327,7 @@ function OverviewPoster({
|
||||
unoptimized
|
||||
loading="eager"
|
||||
sizes="1024px"
|
||||
className="object-cover"
|
||||
className="object-contain"
|
||||
/>
|
||||
{[...blendLayers].reverse().map((layer) => (
|
||||
<Image
|
||||
@@ -351,7 +338,7 @@ function OverviewPoster({
|
||||
unoptimized
|
||||
loading="eager"
|
||||
sizes="1024px"
|
||||
className="object-cover"
|
||||
className="object-contain"
|
||||
style={{
|
||||
mixBlendMode: layer.mode,
|
||||
opacity: layer.amount / 100,
|
||||
@@ -549,8 +536,7 @@ function OverviewPoster({
|
||||
})}
|
||||
</div>
|
||||
<p
|
||||
ref={disclaimerRef}
|
||||
className="mt-[1.5cqw] whitespace-pre text-center text-[2.15cqw] leading-[1.15]"
|
||||
className="mt-[1.5cqw] w-max whitespace-pre text-center text-[2.03167cqw] leading-[1.15]"
|
||||
>
|
||||
{DISCLAIMER}
|
||||
</p>
|
||||
@@ -668,14 +654,17 @@ function SortableBlendLayerRow({
|
||||
<FieldLabel htmlFor={`overview-blend-amount-${layer.id}`}>
|
||||
ความเข้ม {layer.amount}%
|
||||
</FieldLabel>
|
||||
<Input
|
||||
<Slider
|
||||
id={`overview-blend-amount-${layer.id}`}
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
step={5}
|
||||
value={layer.amount}
|
||||
onChange={(event) => onUpdate({ amount: Number(event.target.value) })}
|
||||
value={[layer.amount]}
|
||||
onValueChange={(value) =>
|
||||
onUpdate({
|
||||
amount: typeof value === "number" ? value : value[0] ?? 0,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
@@ -738,23 +727,20 @@ export function OverviewImageGenerator({
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [backgroundUrl, setBackgroundUrl] = useState<string | null>(null);
|
||||
const [fadeColor, setFadeColor] = useState("#0E0200");
|
||||
const [fadeTint, setFadeTint] = useState(100);
|
||||
const [fadeTint, setFadeTint] = useState(0);
|
||||
const [auraUpOffset, setAuraUpOffset] = useState(4);
|
||||
const [characterScale, setCharacterScale] = useState(100);
|
||||
const [characterOffset, setCharacterOffset] = useState<Position>({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const [fadeOffset, setFadeOffset] = useState<Position>({ x: 0, y: 0 });
|
||||
const [activePosterLayer, setActivePosterLayer] =
|
||||
useState<PosterLayer>("character");
|
||||
const [disclaimerFontSize, setDisclaimerFontSize] = useState<number | null>(
|
||||
null,
|
||||
);
|
||||
const [fadeOffset, setFadeOffset] = useState<Position>(DEFAULT_FADE_OFFSET);
|
||||
const composition: Composition = {
|
||||
backgroundUrl,
|
||||
fadeColor,
|
||||
fadeTint,
|
||||
auraUpOffset,
|
||||
characterScale: characterScale / 100,
|
||||
characterOffset,
|
||||
fadeOffset,
|
||||
};
|
||||
@@ -843,7 +829,7 @@ export function OverviewImageGenerator({
|
||||
|
||||
function resetPosterPositions() {
|
||||
setCharacterOffset({ x: 0, y: 0 });
|
||||
setFadeOffset({ x: 0, y: 0 });
|
||||
setFadeOffset(DEFAULT_FADE_OFFSET);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -900,6 +886,10 @@ export function OverviewImageGenerator({
|
||||
});
|
||||
if (target === "artwork") {
|
||||
detectArtworkTheme(image, image.naturalWidth, image.naturalHeight);
|
||||
setCharacterScale(100);
|
||||
setArtworkUrl(url);
|
||||
setArtworkError(null);
|
||||
return;
|
||||
}
|
||||
setArtworkError(null);
|
||||
setCropArea(null);
|
||||
@@ -1146,11 +1136,30 @@ export function OverviewImageGenerator({
|
||||
onChange={(event) => selectArtwork(event)}
|
||||
/>
|
||||
<FieldDescription>
|
||||
ทุกภาพจะเปิดหน้าต่างให้ครอป ซูม และจัดตำแหน่งก่อนใช้งาน
|
||||
ใช้ภาพเต็มโดยไม่ครอป และสามารถลากเพื่อจัดตำแหน่งในตัวอย่างได้
|
||||
</FieldDescription>
|
||||
{artworkError ? <FieldError>{artworkError}</FieldError> : null}
|
||||
</Field>
|
||||
|
||||
<Field data-disabled={!artworkUrl}>
|
||||
<FieldLabel htmlFor="overview-artwork-zoom">
|
||||
ซูมภาพตัวละคร {characterScale}%
|
||||
</FieldLabel>
|
||||
<Slider
|
||||
id="overview-artwork-zoom"
|
||||
min={25}
|
||||
max={300}
|
||||
step={1}
|
||||
value={[characterScale]}
|
||||
disabled={!artworkUrl}
|
||||
onValueChange={(value) =>
|
||||
setCharacterScale(
|
||||
typeof value === "number" ? value : value[0] ?? 100,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="overview-background">
|
||||
อัปโหลดพื้นหลัง
|
||||
@@ -1197,29 +1206,31 @@ export function OverviewImageGenerator({
|
||||
<FieldLabel htmlFor="overview-fade-tint">
|
||||
ความเข้มสี Fade {fadeTint}%
|
||||
</FieldLabel>
|
||||
<Input
|
||||
<Slider
|
||||
id="overview-fade-tint"
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={fadeTint}
|
||||
onChange={(event) => setFadeTint(Number(event.target.value))}
|
||||
value={[fadeTint]}
|
||||
onValueChange={(value) =>
|
||||
setFadeTint(typeof value === "number" ? value : value[0] ?? 0)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="overview-aura-up">
|
||||
เลื่อน Aura ด้านบนขึ้น {auraUpOffset}%
|
||||
</FieldLabel>
|
||||
<Input
|
||||
<Slider
|
||||
id="overview-aura-up"
|
||||
type="range"
|
||||
min={0}
|
||||
max={7}
|
||||
step={0.1}
|
||||
value={auraUpOffset}
|
||||
onChange={(event) =>
|
||||
setAuraUpOffset(Number(event.target.value))
|
||||
value={[auraUpOffset]}
|
||||
onValueChange={(value) =>
|
||||
setAuraUpOffset(
|
||||
typeof value === "number" ? value : value[0] ?? 0,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
@@ -1237,36 +1248,6 @@ export function OverviewImageGenerator({
|
||||
เลือกเลเยอร์ใต้ตัวอย่าง แล้วลากในตัวอย่างเพื่อเลื่อนตำแหน่ง
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="overview-disclaimer-size">
|
||||
ขนาด Disclaimer {disclaimerFontSize === null ? "ตามชื่ออัตโนมัติ" : `${disclaimerFontSize}cqw`}
|
||||
</FieldLabel>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
id="overview-disclaimer-size"
|
||||
type="range"
|
||||
min={0.5}
|
||||
max={5}
|
||||
step={0.05}
|
||||
value={disclaimerFontSize ?? 2.15}
|
||||
onChange={(event) =>
|
||||
setDisclaimerFontSize(Number(event.target.value))
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={disclaimerFontSize === null}
|
||||
onClick={() => setDisclaimerFontSize(null)}
|
||||
>
|
||||
อัตโนมัติ
|
||||
</Button>
|
||||
</div>
|
||||
<FieldDescription>
|
||||
ค่าเริ่มต้นจะปรับให้พอดีกับความกว้างชื่ออัตโนมัติ
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel>Role</FieldLabel>
|
||||
<ToggleGroup
|
||||
@@ -1373,32 +1354,30 @@ export function OverviewImageGenerator({
|
||||
nameCardCrop={nameCardCrop}
|
||||
nameCardSize={nameCardSize}
|
||||
elementIconUrls={elementIconUrls}
|
||||
disclaimerFontSize={disclaimerFontSize}
|
||||
interactive
|
||||
activeLayer={activePosterLayer}
|
||||
activeLayer="character"
|
||||
onLayerMove={movePosterLayer}
|
||||
/>
|
||||
</div>
|
||||
<Field className="mt-4">
|
||||
<FieldLabel>เลือกเลเยอร์ที่จะเลื่อน</FieldLabel>
|
||||
<ToggleGroup
|
||||
value={[activePosterLayer]}
|
||||
onValueChange={(value) => {
|
||||
const selected = value[0] as PosterLayer | undefined;
|
||||
if (selected) setActivePosterLayer(selected);
|
||||
}}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
<ToggleGroupItem value="character" className="flex-1">
|
||||
Char
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="fade" className="flex-1">
|
||||
Fade
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
<FieldLabel htmlFor="overview-fade-y">
|
||||
ตำแหน่ง Fade แนวตั้ง {fadeOffset.y}%
|
||||
</FieldLabel>
|
||||
<Slider
|
||||
id="overview-fade-y"
|
||||
min={-20}
|
||||
max={20}
|
||||
step={0.1}
|
||||
value={[fadeOffset.y]}
|
||||
onValueChange={(value) =>
|
||||
setFadeOffset((current) => ({
|
||||
...current,
|
||||
y: typeof value === "number" ? value : value[0] ?? 2,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<FieldDescription>
|
||||
เลือกเลเยอร์แล้วลากตรงบริเวณตัวอย่างเพื่อเลื่อนตำแหน่ง
|
||||
ลากในตัวอย่างเพื่อเลื่อนตัวละคร และใช้แถบนี้เพื่อเลื่อน Fade
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
</CardContent>
|
||||
@@ -1453,15 +1432,16 @@ export function OverviewImageGenerator({
|
||||
<FieldLabel htmlFor="overview-element-gradient-angle">
|
||||
มุมสีไล่ระดับธาตุ {elementGradientAngle}°
|
||||
</FieldLabel>
|
||||
<Input
|
||||
<Slider
|
||||
id="overview-element-gradient-angle"
|
||||
type="range"
|
||||
min={0}
|
||||
max={360}
|
||||
step={1}
|
||||
value={elementGradientAngle}
|
||||
onChange={(event) =>
|
||||
setElementGradientAngle(Number(event.target.value))
|
||||
value={[elementGradientAngle]}
|
||||
onValueChange={(value) =>
|
||||
setElementGradientAngle(
|
||||
typeof value === "number" ? value : value[0] ?? 0,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
@@ -1487,7 +1467,6 @@ export function OverviewImageGenerator({
|
||||
nameCardCrop={nameCardCrop}
|
||||
nameCardSize={nameCardSize}
|
||||
elementIconUrls={elementIconUrls}
|
||||
disclaimerFontSize={disclaimerFontSize}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -42,10 +42,8 @@ export function ArtifactImageExport({
|
||||
const [sections, setSections] = useState<Set<ArtifactExportSection>>(
|
||||
() => new Set(sectionOptions.map(([value]) => value)),
|
||||
);
|
||||
const [includeNote, setIncludeNote] = useState(true);
|
||||
const [recommendedCondition, setRecommendedCondition] = useState("0");
|
||||
const [targetCondition, setTargetCondition] = useState("0");
|
||||
const showNote = hasNote && includeNote;
|
||||
const entireSection = sections.size === sectionOptions.length;
|
||||
const hiddenSections = sectionOptions
|
||||
.filter(([value]) => !sections.has(value))
|
||||
@@ -53,9 +51,6 @@ export function ArtifactImageExport({
|
||||
.join("");
|
||||
const targetRule = `[data-target-condition]:not([data-target-condition="${targetCondition}"]){display:none!important}`;
|
||||
const recommendedRule = `[data-recommended-condition]:not([data-recommended-condition="${recommendedCondition}"]){display:none!important}`;
|
||||
const noteRule = showNote
|
||||
? ""
|
||||
: '[data-artifact-export-section="note"]{display:none!important}';
|
||||
const onlySection = sections.size === 1 ? [...sections][0] : null;
|
||||
const exportWidth = onlySection === "best-stats"
|
||||
? 700
|
||||
@@ -63,7 +58,7 @@ export function ArtifactImageExport({
|
||||
? 800
|
||||
: 1200;
|
||||
const exportHeight = onlySection === "best-stats" ? 700 : undefined;
|
||||
const hideEmptyRightColumn = !showNote && !sections.has("target-stats") && !sections.has("talents")
|
||||
const hideEmptyRightColumn = !hasNote && !sections.has("target-stats") && !sections.has("talents")
|
||||
? ".artifact-export-right{display:none!important}"
|
||||
: "";
|
||||
const layoutRules = onlySection === "tierlist"
|
||||
@@ -115,16 +110,6 @@ export function ArtifactImageExport({
|
||||
<FieldLabel htmlFor={`artifact-export-${value}`}>{label}</FieldLabel>
|
||||
</Field>
|
||||
))}
|
||||
{hasNote ? (
|
||||
<Field orientation="horizontal">
|
||||
<Checkbox
|
||||
id="artifact-export-note"
|
||||
checked={includeNote}
|
||||
onCheckedChange={(checked) => setIncludeNote(checked === true)}
|
||||
/>
|
||||
<FieldLabel htmlFor="artifact-export-note">รวมโน้ต</FieldLabel>
|
||||
</Field>
|
||||
) : null}
|
||||
</FieldGroup>
|
||||
{sections.has("best-stats") && recommendedConditionLabels.length > 1 ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -177,7 +162,7 @@ export function ArtifactImageExport({
|
||||
disabled={sections.size === 0}
|
||||
captureClassName="p-0"
|
||||
>
|
||||
<style>{hiddenSections + targetRule + recommendedRule + noteRule + hideEmptyRightColumn + layoutRules}</style>
|
||||
<style>{hiddenSections + targetRule + recommendedRule + hideEmptyRightColumn + layoutRules}</style>
|
||||
{children}
|
||||
</GuideImageExport>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Image from "next/image";
|
||||
|
||||
import { GuideExportBranding } from "@/components/public/guide-export-branding";
|
||||
import { ExportText } from "@/components/public/export-language";
|
||||
import type { PublicGuide } from "@/lib/guides/queries";
|
||||
import { constellationChartRows } from "@/lib/guides/export-image";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -45,7 +46,7 @@ export function ConstellationImage({
|
||||
{`${guide.character.name}’s Constellations`}
|
||||
</h2>
|
||||
<p className="mt-3 line-clamp-2 max-w-[1100px] whitespace-pre-line text-[42px] leading-tight font-bold text-[#f1badd]">
|
||||
Team Damage
|
||||
<ExportText th="ดาเมจทั้งทีม" en="Team Damage" />
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
||||
|
||||
export type ExportLanguage = "th" | "en";
|
||||
|
||||
const STORAGE_KEY = "buzz-sheet:export-language";
|
||||
const LANGUAGE_EVENT = "buzz-sheet:export-language-change";
|
||||
let memoryLanguage: ExportLanguage = "th";
|
||||
|
||||
function storedLanguage(): ExportLanguage {
|
||||
if (typeof window === "undefined") return "th";
|
||||
try {
|
||||
memoryLanguage = window.localStorage.getItem(STORAGE_KEY) === "en" ? "en" : "th";
|
||||
return memoryLanguage;
|
||||
} catch {
|
||||
return memoryLanguage;
|
||||
}
|
||||
}
|
||||
|
||||
export function useExportLanguage() {
|
||||
const language = useSyncExternalStore(
|
||||
(onStoreChange) => {
|
||||
window.addEventListener(LANGUAGE_EVENT, onStoreChange);
|
||||
window.addEventListener("storage", onStoreChange);
|
||||
return () => {
|
||||
window.removeEventListener(LANGUAGE_EVENT, onStoreChange);
|
||||
window.removeEventListener("storage", onStoreChange);
|
||||
};
|
||||
},
|
||||
storedLanguage,
|
||||
() => "th",
|
||||
);
|
||||
|
||||
function setLanguage(next: ExportLanguage) {
|
||||
memoryLanguage = next;
|
||||
try {
|
||||
window.localStorage.setItem(STORAGE_KEY, next);
|
||||
} catch {
|
||||
// Browser storage is optional; the current export still uses the selection.
|
||||
} finally {
|
||||
window.dispatchEvent(new CustomEvent(LANGUAGE_EVENT, { detail: next }));
|
||||
}
|
||||
}
|
||||
|
||||
return [language, setLanguage] as const;
|
||||
}
|
||||
|
||||
export function ExportLanguageToggle({
|
||||
language,
|
||||
onChange,
|
||||
}: {
|
||||
language: ExportLanguage;
|
||||
onChange: (language: ExportLanguage) => void;
|
||||
}) {
|
||||
return (
|
||||
<ToggleGroup
|
||||
aria-label="ภาษาในภาพส่งออก"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
value={[language]}
|
||||
onValueChange={(values) => {
|
||||
const next = values[0];
|
||||
if (next === "th" || next === "en") onChange(next);
|
||||
}}
|
||||
>
|
||||
<ToggleGroupItem
|
||||
value="th"
|
||||
>
|
||||
ไทย
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="en"
|
||||
>
|
||||
English
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExportText({ th, en }: { th: string; en: string }) {
|
||||
return (
|
||||
<>
|
||||
<span data-export-lang="th">{th}</span>
|
||||
<span data-export-lang="en">{en}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,11 @@ import { DownloadIcon } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
ExportLanguageToggle,
|
||||
useExportLanguage,
|
||||
type ExportLanguage,
|
||||
} from "@/components/public/export-language";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -21,6 +26,21 @@ function downloadFileName(value: string): string {
|
||||
return value.replace(/[\\/:*?"<>|]/gu, "-");
|
||||
}
|
||||
|
||||
export function imageExportFileName(
|
||||
fileName: string,
|
||||
language: ExportLanguage,
|
||||
withoutNotes: boolean,
|
||||
localized = true,
|
||||
): string {
|
||||
const selectedFileName = withoutNotes ? withoutNotesFileName(fileName) : fileName;
|
||||
const languageFileName = localized
|
||||
? (/\.[^.]+$/u.test(selectedFileName)
|
||||
? selectedFileName.replace(/(\.[^.]+)$/u, `-${language}$1`)
|
||||
: `${selectedFileName}-${language}`)
|
||||
: selectedFileName;
|
||||
return downloadFileName(languageFileName);
|
||||
}
|
||||
|
||||
export function GuideImageExport({
|
||||
children,
|
||||
fileName,
|
||||
@@ -31,6 +51,9 @@ export function GuideImageExport({
|
||||
allowWithoutNotes = true,
|
||||
captureClassName,
|
||||
buttonLabel = "ส่งออกภาพ",
|
||||
language: controlledLanguage,
|
||||
showLanguageSelector = true,
|
||||
localizeFileName = true,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
fileName: string;
|
||||
@@ -41,12 +64,17 @@ export function GuideImageExport({
|
||||
allowWithoutNotes?: boolean;
|
||||
captureClassName?: string;
|
||||
buttonLabel?: string;
|
||||
language?: ExportLanguage;
|
||||
showLanguageSelector?: boolean;
|
||||
localizeFileName?: boolean;
|
||||
}) {
|
||||
const captureRef = useRef<HTMLDivElement>(null);
|
||||
const [captureMode, setCaptureMode] = useState<
|
||||
"with-notes" | "without-notes" | null
|
||||
>(null);
|
||||
const capturing = captureMode !== null;
|
||||
const [storedLanguage, setStoredLanguage] = useExportLanguage();
|
||||
const language = controlledLanguage ?? storedLanguage;
|
||||
|
||||
useEffect(() => {
|
||||
if (!capturing) return;
|
||||
@@ -88,10 +116,11 @@ export function GuideImageExport({
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement("a");
|
||||
link.download = downloadFileName(
|
||||
captureMode === "without-notes"
|
||||
? withoutNotesFileName(fileName)
|
||||
: fileName,
|
||||
link.download = imageExportFileName(
|
||||
fileName,
|
||||
language,
|
||||
captureMode === "without-notes",
|
||||
localizeFileName,
|
||||
);
|
||||
link.href = url;
|
||||
document.body.append(link);
|
||||
@@ -109,10 +138,13 @@ export function GuideImageExport({
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [capturing, captureMode, fileName, format, height, width]);
|
||||
}, [capturing, captureMode, fileName, format, height, language, localizeFileName, width]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{showLanguageSelector ? (
|
||||
<ExportLanguageToggle language={language} onChange={setStoredLanguage} />
|
||||
) : null}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(["with-notes", "without-notes"] as const).filter((mode) => allowWithoutNotes || mode === "with-notes").map((mode) => (
|
||||
<Button
|
||||
@@ -144,12 +176,14 @@ export function GuideImageExport({
|
||||
>
|
||||
<div
|
||||
ref={captureRef}
|
||||
data-export-language={language}
|
||||
className={cn(
|
||||
"bg-background p-10 text-foreground",
|
||||
captureClassName,
|
||||
)}
|
||||
style={{ width, height }}
|
||||
>
|
||||
<style>{'[data-export-language="th"] [data-export-lang="en"],[data-export-language="en"] [data-export-lang="th"]{display:none!important}'}</style>
|
||||
{children}
|
||||
</div>
|
||||
</div>,
|
||||
|
||||
@@ -26,6 +26,7 @@ import { SiteHeader } from "@/components/public/site-header";
|
||||
import { ScrollableNav } from "@/components/public/scrollable-nav";
|
||||
import { TeamArtifactIcon, TeamSearch } from "@/components/public/team-search";
|
||||
import { GuideImageExport } from "@/components/public/guide-image-export";
|
||||
import { ExportText } from "@/components/public/export-language";
|
||||
import { WeaponImageExport } from "@/components/public/weapon-image-export";
|
||||
import { WeaponChart } from "@/components/public/weapon-chart";
|
||||
import { WeaponDisplayMode, WeaponDisplayModeProvider } from "@/components/public/weapon-display-mode";
|
||||
@@ -248,6 +249,8 @@ function GuideOverview({
|
||||
format="png"
|
||||
fileName={`${guide.name}-materials.png`}
|
||||
allowWithoutNotes={false}
|
||||
showLanguageSelector={false}
|
||||
localizeFileName={false}
|
||||
captureClassName="p-0"
|
||||
>
|
||||
<MaterialPoster
|
||||
@@ -358,6 +361,10 @@ function metric(value: string | null, locale?: string) {
|
||||
: `${Number(value).toLocaleString(locale, { maximumFractionDigits: 2 })}%`;
|
||||
}
|
||||
|
||||
function exportLabel(exporting: boolean, th: string, en: string): ReactNode {
|
||||
return exporting ? <ExportText th={th} en={en} /> : th;
|
||||
}
|
||||
|
||||
function lunarisItemUrl(
|
||||
kind: "character" | "weapon" | "artifact",
|
||||
key: string,
|
||||
@@ -409,7 +416,7 @@ function WeaponSectionContent({
|
||||
<header className="relative z-10 flex items-center justify-center text-center">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<p className="text-sm font-medium text-primary">
|
||||
{exporting ? "Recommended Weapons" : "อาวุธที่แนะนำ"}
|
||||
อาวุธที่แนะนำ
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<Image
|
||||
@@ -425,7 +432,7 @@ function WeaponSectionContent({
|
||||
)}
|
||||
/>
|
||||
<h1 className="whitespace-nowrap font-heading text-4xl font-semibold">
|
||||
{exporting ? "Weapons" : "อาวุธ"}
|
||||
Weapons
|
||||
</h1>
|
||||
</div>
|
||||
{note ? (
|
||||
@@ -640,31 +647,31 @@ function ArtifactSectionContent({
|
||||
const recommendedConditions = profile?.recommendedConditions ?? [];
|
||||
const recommendedCards = recommendedConditions.map((condition, conditionIndex) => (
|
||||
<div key={conditionIndex} data-artifact-export-section="best-stats" data-recommended-condition={conditionIndex} className="flex flex-col gap-5">
|
||||
{exporting && recommendedConditions.length > 1 ? <p className="text-sm font-medium">{condition.name || `Condition ${conditionIndex + 1}`}</p> : null}
|
||||
{exporting && recommendedConditions.length > 1 ? <p className="text-sm font-medium">{condition.name || <ExportText th={`เงื่อนไข ${conditionIndex + 1}`} en={`Condition ${conditionIndex + 1}`} />}</p> : null}
|
||||
{condition.note ? <p data-export-note className="whitespace-pre-wrap text-sm text-muted-foreground">{condition.note}</p> : null}
|
||||
{profile ? (
|
||||
<Card data-artifact-export-section="best-stats" className="artifact-export-main-stats overflow-hidden pt-0">
|
||||
<CardHeader className="bg-primary py-4 text-center text-primary-foreground">
|
||||
<CardTitle className="text-2xl">{exporting ? "Best Main Stats" : "ออฟหลักที่ดีที่สุด"}</CardTitle>
|
||||
<CardTitle className="text-2xl">{exportLabel(exporting, "ออฟหลักที่ดีที่สุด", "Best Main Stats")}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-3 p-0">
|
||||
<ArtifactStat
|
||||
iconSrc="/icon/Icon_Sands_of_Eon.webp"
|
||||
label={exporting ? "Sands" : "นาฬิกา"}
|
||||
label={exportLabel(exporting, "นาฬิกา", "Sands")}
|
||||
values={condition.sands}
|
||||
exporting={exporting}
|
||||
elementIconUrls={guide.elementIconUrls}
|
||||
/>
|
||||
<ArtifactStat
|
||||
iconSrc="/icon/Icon_Goblet_of_Eonothem.webp"
|
||||
label={exporting ? "Goblet" : "ถ้วย"}
|
||||
label={exportLabel(exporting, "ถ้วย", "Goblet")}
|
||||
values={condition.goblet}
|
||||
exporting={exporting}
|
||||
elementIconUrls={guide.elementIconUrls}
|
||||
/>
|
||||
<ArtifactStat
|
||||
iconSrc="/icon/Icon_Circlet_of_Logos.webp"
|
||||
label={exporting ? "Circlet" : "หัว"}
|
||||
label={exportLabel(exporting, "หัว", "Circlet")}
|
||||
values={condition.circlet}
|
||||
exporting={exporting}
|
||||
elementIconUrls={guide.elementIconUrls}
|
||||
@@ -675,7 +682,7 @@ function ArtifactSectionContent({
|
||||
{condition.substats.length ? (
|
||||
<Card data-artifact-export-section="best-stats" className="artifact-export-substats">
|
||||
<CardContent className="flex flex-col items-center gap-2 py-5 text-center text-sm">
|
||||
<strong>{exporting ? "Best Substats" : "ออฟรองที่ดีที่สุด"}</strong>
|
||||
<strong>{exportLabel(exporting, "ออฟรองที่ดีที่สุด", "Best Substats")}</strong>
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
{condition.substats.map((stat, index) => (
|
||||
<ArtifactStatValue
|
||||
@@ -710,7 +717,7 @@ function ArtifactSectionContent({
|
||||
<header className="relative z-10 flex items-center justify-center text-center">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<p className="text-sm font-medium text-primary">
|
||||
{exporting ? "Recommended Artifacts & Stats" : "อาร์ติแฟกต์และสแตทที่แนะนำ"}
|
||||
อาร์ติแฟกต์และสแตทที่แนะนำ
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<Image
|
||||
@@ -726,7 +733,7 @@ function ArtifactSectionContent({
|
||||
)}
|
||||
/>
|
||||
<h1 className="whitespace-nowrap font-heading text-4xl font-semibold">
|
||||
{exporting ? "Artifacts & Stats" : "อาร์ติแฟกต์และค่าสถานะ"}
|
||||
Artifacts & Stats
|
||||
</h1>
|
||||
</div>
|
||||
{action}
|
||||
@@ -751,9 +758,9 @@ function ArtifactSectionContent({
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{group.name === "Late Game"
|
||||
? (exporting ? "Late Game" : "เลทเกม")
|
||||
? exportLabel(exporting, "เลทเกม", "Late Game")
|
||||
: group.name === "Early Game"
|
||||
? (exporting ? "Early Game" : "ต้นเกม")
|
||||
? exportLabel(exporting, "ต้นเกม", "Early Game")
|
||||
: group.name}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
@@ -806,7 +813,7 @@ function ArtifactSectionContent({
|
||||
exporting ? (
|
||||
profile.targetConditions.filter((group) => group.rows.length).map((group, groupIndex) => (
|
||||
<div key={groupIndex} data-artifact-export-section="target-stats" data-target-condition={groupIndex}>
|
||||
<p className="mb-2 text-sm font-medium">{exporting ? "Recommended Minimum Stats" : "ค่าสถานะต่ำๆที่ควรจะปั้นไว้"}</p>
|
||||
<p className="mb-2 text-sm font-medium">{exportLabel(exporting, "ค่าสถานะต่ำๆที่ควรจะปั้นไว้", "Recommended Minimum Stats")}</p>
|
||||
{group.note ? <p data-export-note className="mb-2 whitespace-pre-wrap text-sm text-muted-foreground">{group.note}</p> : null}
|
||||
<div className="flex flex-col gap-1">
|
||||
{group.rows.map((target, index) => (
|
||||
@@ -844,7 +851,7 @@ function ArtifactSectionContent({
|
||||
<ConditionSwitcher label="เลือกเงื่อนไขค่าสถานะเป้าหมาย" labels={profile.targetConditions.filter((group) => group.rows.length).map((group, index) => group.name || `เงื่อนไข ${index + 1}`)}>
|
||||
{profile.targetConditions.filter((group) => group.rows.length).map((group, groupIndex) => (
|
||||
<div key={groupIndex}>
|
||||
<p className="mb-2 text-sm font-medium">{exporting ? "Recommended Minimum Stats" : "ค่าสถานะต่ำๆที่ควรจะปั้นไว้"}</p>
|
||||
<p className="mb-2 text-sm font-medium">{exportLabel(exporting, "ค่าสถานะต่ำๆที่ควรจะปั้นไว้", "Recommended Minimum Stats")}</p>
|
||||
{group.note ? <p data-export-note className="mb-2 whitespace-pre-wrap text-sm text-muted-foreground">{group.note}</p> : null}
|
||||
<div className="flex flex-col gap-1">
|
||||
{group.rows.map((target, index) => (
|
||||
@@ -865,7 +872,7 @@ function ArtifactSectionContent({
|
||||
{profile.talents.length ? (
|
||||
<div data-artifact-export-section="talents">
|
||||
<p className="mb-2 text-sm font-medium">
|
||||
{exporting ? "Talent Priority" : "ลำดับการอัพพรสวรรค์"}
|
||||
{exportLabel(exporting, "ลำดับการอัพพรสวรรค์", "Talent Priority")}
|
||||
</p>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -915,7 +922,7 @@ function ArtifactSectionContent({
|
||||
</div>
|
||||
<Badge variant="secondary">
|
||||
{talent.level
|
||||
? (exporting ? `Level ${talent.level}+` : `เลเวล ${talent.level}+`)
|
||||
? exportLabel(exporting, `เลเวล ${talent.level}+`, `Level ${talent.level}+`)
|
||||
: "-"}
|
||||
</Badge>
|
||||
<p
|
||||
@@ -933,10 +940,10 @@ function ArtifactSectionContent({
|
||||
)}
|
||||
>
|
||||
{talent.type === "E"
|
||||
? (exporting ? "Elemental Skill" : "สกิลธาตุ")
|
||||
? exportLabel(exporting, "สกิลธาตุ", "Elemental Skill")
|
||||
: talent.type === "Q"
|
||||
? (exporting ? "Elemental Burst" : "ท่าไม้ตาย")
|
||||
: (exporting ? "Normal Attack" : "ตีปกติ")}
|
||||
? exportLabel(exporting, "ท่าไม้ตาย", "Elemental Burst")
|
||||
: exportLabel(exporting, "ตีปกติ", "Normal Attack")}
|
||||
</p>
|
||||
</div>
|
||||
{index < talents.length - 1 ? (
|
||||
@@ -967,7 +974,7 @@ function ArtifactSectionContent({
|
||||
) : null}
|
||||
{profile.note ? (
|
||||
<div data-export-note data-artifact-export-section="note" className="pt-3">
|
||||
<p className="mb-2 text-sm font-medium">{exporting ? "Notes" : "โน็ต"}</p>
|
||||
<p className="mb-2 text-sm font-medium">{exportLabel(exporting, "โน็ต", "Notes")}</p>
|
||||
<p className="text-sm leading-6 text-muted-foreground">
|
||||
{profile.note}
|
||||
</p>
|
||||
@@ -1016,8 +1023,14 @@ const englishStats: Record<string, string> = {
|
||||
"CRIT Rate%/CRIT DMG%": "CRIT Rate / CRIT DMG",
|
||||
};
|
||||
|
||||
function statLabel(value: string, exporting = false): string {
|
||||
return (exporting ? englishStats : thaiStats)[value] ?? value;
|
||||
function statLabel(value: string, exporting = false): ReactNode {
|
||||
if (!exporting) return thaiStats[value] ?? value;
|
||||
return (
|
||||
<ExportText
|
||||
th={thaiStats[value] ?? value}
|
||||
en={englishStats[value] ?? value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ArtifactStat({
|
||||
@@ -1028,7 +1041,7 @@ function ArtifactStat({
|
||||
elementIconUrls,
|
||||
}: {
|
||||
iconSrc: string;
|
||||
label: string;
|
||||
label: ReactNode;
|
||||
values: string[];
|
||||
exporting?: boolean;
|
||||
elementIconUrls: Record<string, string>;
|
||||
@@ -1143,7 +1156,7 @@ function ConstellationSection({ guide }: { guide: PublicGuide }) {
|
||||
className="size-14 shrink-0 object-contain"
|
||||
/>
|
||||
<h1 className="whitespace-nowrap font-heading text-4xl font-semibold">
|
||||
กลุ่มดาว
|
||||
Constellations
|
||||
</h1>
|
||||
</div>
|
||||
{note ? (
|
||||
@@ -1215,7 +1228,7 @@ function TeamSection({ guide }: { guide: PublicGuide }) {
|
||||
<section className="flex flex-col gap-6">
|
||||
<header className="text-center">
|
||||
<p className="text-sm font-medium text-primary">คำนวณความเสียหาย</p>
|
||||
<h1 className="font-heading text-4xl font-semibold">คำนวณทีม</h1>
|
||||
<h1 className="font-heading text-4xl font-semibold">Team Calculation</h1>
|
||||
</header>
|
||||
<TeamSearch groups={guide.teamGroups} />
|
||||
</section>
|
||||
@@ -1261,7 +1274,7 @@ function ExtraSectionContent({
|
||||
/>
|
||||
) : null}
|
||||
<header className="relative z-10 flex flex-col items-center gap-3 text-center">
|
||||
<p className="text-sm font-medium text-primary">{exporting ? "Additional Guide" : "คู่มือเสริม"}</p>
|
||||
<p className="text-sm font-medium text-primary">คู่มือเสริม</p>
|
||||
<h1 className="font-heading text-4xl font-semibold">{extra.title}</h1>
|
||||
{action}
|
||||
</header>
|
||||
@@ -1312,7 +1325,7 @@ function ExtraSectionContent({
|
||||
) : null}
|
||||
<div className="min-w-0">
|
||||
<p className="truncate text-sm font-medium">{item.name}</p>
|
||||
{!item.available ? <Badge variant="secondary">{exporting ? "Item unavailable" : "ไม่พบรายการ"}</Badge> : null}
|
||||
{!item.available ? <Badge variant="secondary">{exportLabel(exporting, "ไม่พบรายการ", "Item unavailable")}</Badge> : null}
|
||||
</div>
|
||||
<ArrowUpRightIcon aria-hidden="true" className="ml-auto shrink-0" />
|
||||
</a>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ArrowUpRightIcon, SearchIcon, XIcon } from "lucide-react";
|
||||
import { useMemo, useState, type ReactNode } from "react";
|
||||
|
||||
import { GuideImageExport } from "@/components/public/guide-image-export";
|
||||
import { ExportText } from "@/components/public/export-language";
|
||||
import { ScrollableNav } from "@/components/public/scrollable-nav";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -168,7 +169,12 @@ function TeamExportImage({ team }: { team: Team }) {
|
||||
<div className="flex min-h-337.5 w-270 flex-col bg-[#05060a] p-[48px_52px] font-heading text-[white]">
|
||||
<header className="flex flex-col items-center text-center">
|
||||
<h2 className="text-[64px] leading-tight font-bold">{team.title}</h2>
|
||||
<p className="text-[32px] text-[#b9b9c2]">{`Rotation duration: ${seconds.toLocaleString("en-US", { maximumFractionDigits: 2 })} seconds`}</p>
|
||||
<p className="text-[32px] text-[#b9b9c2]">
|
||||
<ExportText
|
||||
th={`ระยะเวลาโรเทชั่น: ${seconds.toLocaleString("en-US", { maximumFractionDigits: 2 })} วินาที`}
|
||||
en={`Rotation duration: ${seconds.toLocaleString("en-US", { maximumFractionDigits: 2 })} seconds`}
|
||||
/>
|
||||
</p>
|
||||
</header>
|
||||
<div className="mt-5 flex flex-col gap-4">
|
||||
{team.members.map((member) => {
|
||||
|
||||
@@ -2,8 +2,14 @@
|
||||
|
||||
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 {
|
||||
@@ -35,7 +41,7 @@ function WeaponComparisonImage({
|
||||
guide: PublicGuide;
|
||||
group: WeaponGroup;
|
||||
metric: DamageMetric;
|
||||
conditionLabel: string;
|
||||
conditionLabel: ReactNode;
|
||||
}) {
|
||||
const values = group.rows.map((row) => Number(row[metric]) || 0);
|
||||
const maximum = Math.max(100, ...values);
|
||||
@@ -48,7 +54,11 @@ function WeaponComparisonImage({
|
||||
{`${guide.character.name}’s Weapon Comparison`}
|
||||
</h2>
|
||||
<p className="mt-1 text-4xl font-bold text-white">
|
||||
{metric === "overallPercent" ? "Overall Damage" : "Personal Damage"}
|
||||
{metric === "overallPercent" ? (
|
||||
<ExportText th="ดาเมจทั้งทีม" en="Overall Damage" />
|
||||
) : (
|
||||
<ExportText th="ดาเมจส่วนตัว" en="Personal Damage" />
|
||||
)}
|
||||
</p>
|
||||
<p className="mt-2 text-2xl font-semibold">{conditionLabel}</p>
|
||||
</header>
|
||||
@@ -110,6 +120,7 @@ export function WeaponImageExport({
|
||||
group: WeaponGroup;
|
||||
conditionIndex: number;
|
||||
}) {
|
||||
const [language, setLanguage] = useExportLanguage();
|
||||
const conditionLabel = group.name || `Condition ${conditionIndex + 1}`;
|
||||
const baseName = `${guide.name}-Weapon-${conditionLabel}`;
|
||||
const metricOptions = ([
|
||||
@@ -132,6 +143,7 @@ export function WeaponImageExport({
|
||||
เลือกค่าที่ต้องการใช้สร้างภาพเปรียบเทียบอาวุธ
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<ExportLanguageToggle language={language} onChange={setLanguage} />
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{metricOptions.map(([metric, label]) => (
|
||||
<GuideImageExport
|
||||
@@ -139,13 +151,20 @@ export function WeaponImageExport({
|
||||
fileName={`${baseName}-${label}.png`}
|
||||
width={1200}
|
||||
captureClassName="p-0"
|
||||
language={language}
|
||||
showLanguageSelector={false}
|
||||
buttonLabel={metric === "overallPercent" ? "ดาเมจทั้งทีม" : "ดาเมจส่วนตัว"}
|
||||
>
|
||||
<WeaponComparisonImage
|
||||
guide={guide}
|
||||
group={group}
|
||||
metric={metric}
|
||||
conditionLabel={conditionLabel}
|
||||
conditionLabel={group.name ? conditionLabel : (
|
||||
<ExportText
|
||||
th={`เงื่อนไข ${conditionIndex + 1}`}
|
||||
en={`Condition ${conditionIndex + 1}`}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</GuideImageExport>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user