diff --git a/components/admin/overview-image-generator.tsx b/components/admin/overview-image-generator.tsx index a8dff70..c045946 100644 --- a/components/admin/overview-image-generator.tsx +++ b/components/admin/overview-image-generator.tsx @@ -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; 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(null); - const disclaimerRef = useRef(null); const posterRef = useRef(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 ( @@ -326,6 +312,7 @@ function OverviewPoster({ {[...blendLayers].reverse().map((layer) => (

{DISCLAIMER}

@@ -668,14 +654,17 @@ function SortableBlendLayerRow({ ความเข้ม {layer.amount}% - onUpdate({ amount: Number(event.target.value) })} + value={[layer.amount]} + onValueChange={(value) => + onUpdate({ + amount: typeof value === "number" ? value : value[0] ?? 0, + }) + } /> @@ -738,23 +727,20 @@ export function OverviewImageGenerator({ const [exporting, setExporting] = useState(false); const [backgroundUrl, setBackgroundUrl] = useState(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({ x: 0, y: 0, }); - const [fadeOffset, setFadeOffset] = useState({ x: 0, y: 0 }); - const [activePosterLayer, setActivePosterLayer] = - useState("character"); - const [disclaimerFontSize, setDisclaimerFontSize] = useState( - null, - ); + const [fadeOffset, setFadeOffset] = useState(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)} /> - ทุกภาพจะเปิดหน้าต่างให้ครอป ซูม และจัดตำแหน่งก่อนใช้งาน + ใช้ภาพเต็มโดยไม่ครอป และสามารถลากเพื่อจัดตำแหน่งในตัวอย่างได้ {artworkError ? {artworkError} : null} + + + ซูมภาพตัวละคร {characterScale}% + + + setCharacterScale( + typeof value === "number" ? value : value[0] ?? 100, + ) + } + /> + + อัปโหลดพื้นหลัง @@ -1197,29 +1206,31 @@ export function OverviewImageGenerator({ ความเข้มสี Fade {fadeTint}% - setFadeTint(Number(event.target.value))} + value={[fadeTint]} + onValueChange={(value) => + setFadeTint(typeof value === "number" ? value : value[0] ?? 0) + } /> เลื่อน Aura ด้านบนขึ้น {auraUpOffset}% - - setAuraUpOffset(Number(event.target.value)) + value={[auraUpOffset]} + onValueChange={(value) => + setAuraUpOffset( + typeof value === "number" ? value : value[0] ?? 0, + ) } /> @@ -1237,36 +1248,6 @@ export function OverviewImageGenerator({ เลือกเลเยอร์ใต้ตัวอย่าง แล้วลากในตัวอย่างเพื่อเลื่อนตำแหน่ง - - - ขนาด Disclaimer {disclaimerFontSize === null ? "ตามชื่ออัตโนมัติ" : `${disclaimerFontSize}cqw`} - -
- - setDisclaimerFontSize(Number(event.target.value)) - } - /> - -
- - ค่าเริ่มต้นจะปรับให้พอดีกับความกว้างชื่ออัตโนมัติ - -
Role - เลือกเลเยอร์ที่จะเลื่อน - { - const selected = value[0] as PosterLayer | undefined; - if (selected) setActivePosterLayer(selected); - }} - variant="outline" - className="w-full" - > - - Char - - - Fade - - + + ตำแหน่ง Fade แนวตั้ง {fadeOffset.y}% + + + setFadeOffset((current) => ({ + ...current, + y: typeof value === "number" ? value : value[0] ?? 2, + })) + } + /> - เลือกเลเยอร์แล้วลากตรงบริเวณตัวอย่างเพื่อเลื่อนตำแหน่ง + ลากในตัวอย่างเพื่อเลื่อนตัวละคร และใช้แถบนี้เพื่อเลื่อน Fade @@ -1453,15 +1432,16 @@ export function OverviewImageGenerator({ มุมสีไล่ระดับธาตุ {elementGradientAngle}° - - setElementGradientAngle(Number(event.target.value)) + value={[elementGradientAngle]} + onValueChange={(value) => + setElementGradientAngle( + typeof value === "number" ? value : value[0] ?? 0, + ) } /> @@ -1487,7 +1467,6 @@ export function OverviewImageGenerator({ nameCardCrop={nameCardCrop} nameCardSize={nameCardSize} elementIconUrls={elementIconUrls} - disclaimerFontSize={disclaimerFontSize} /> diff --git a/components/public/artifact-image-export.tsx b/components/public/artifact-image-export.tsx index fa200b0..d789513 100644 --- a/components/public/artifact-image-export.tsx +++ b/components/public/artifact-image-export.tsx @@ -42,10 +42,8 @@ export function ArtifactImageExport({ const [sections, setSections] = useState>( () => 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({ {label} ))} - {hasNote ? ( - - setIncludeNote(checked === true)} - /> - รวมโน้ต - - ) : null} {sections.has("best-stats") && recommendedConditionLabels.length > 1 ? (
@@ -177,7 +162,7 @@ export function ArtifactImageExport({ disabled={sections.size === 0} captureClassName="p-0" > - + {children} diff --git a/components/public/constellation-image.tsx b/components/public/constellation-image.tsx index 653ed5f..d42381d 100644 --- a/components/public/constellation-image.tsx +++ b/components/public/constellation-image.tsx @@ -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`}

- Team Damage +

diff --git a/components/public/export-language.tsx b/components/public/export-language.tsx new file mode 100644 index 0000000..bc987a9 --- /dev/null +++ b/components/public/export-language.tsx @@ -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 ( + { + const next = values[0]; + if (next === "th" || next === "en") onChange(next); + }} + > + + ไทย + + + English + + + ); +} + +export function ExportText({ th, en }: { th: string; en: string }) { + return ( + <> + {th} + {en} + + ); +} diff --git a/components/public/guide-image-export.tsx b/components/public/guide-image-export.tsx index 78db04d..e2729e3 100644 --- a/components/public/guide-image-export.tsx +++ b/components/public/guide-image-export.tsx @@ -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(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 ? ( + + ) : null}
{(["with-notes", "without-notes"] as const).filter((mode) => allowWithoutNotes || mode === "with-notes").map((mode) => (
, diff --git a/components/public/structured-guide.tsx b/components/public/structured-guide.tsx index 82ded9f..1cd6f65 100644 --- a/components/public/structured-guide.tsx +++ b/components/public/structured-guide.tsx @@ -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" > : th; +} + function lunarisItemUrl( kind: "character" | "weapon" | "artifact", key: string, @@ -409,7 +416,7 @@ function WeaponSectionContent({

- {exporting ? "Recommended Weapons" : "อาวุธที่แนะนำ"} + อาวุธที่แนะนำ

- {exporting ? "Weapons" : "อาวุธ"} + Weapons

{note ? ( @@ -640,31 +647,31 @@ function ArtifactSectionContent({ const recommendedConditions = profile?.recommendedConditions ?? []; const recommendedCards = recommendedConditions.map((condition, conditionIndex) => (
- {exporting && recommendedConditions.length > 1 ?

{condition.name || `Condition ${conditionIndex + 1}`}

: null} + {exporting && recommendedConditions.length > 1 ?

{condition.name || }

: null} {condition.note ?

{condition.note}

: null} {profile ? ( - {exporting ? "Best Main Stats" : "ออฟหลักที่ดีที่สุด"} + {exportLabel(exporting, "ออฟหลักที่ดีที่สุด", "Best Main Stats")} - {exporting ? "Best Substats" : "ออฟรองที่ดีที่สุด"} + {exportLabel(exporting, "ออฟรองที่ดีที่สุด", "Best Substats")}
{condition.substats.map((stat, index) => (

- {exporting ? "Recommended Artifacts & Stats" : "อาร์ติแฟกต์และสแตทที่แนะนำ"} + อาร์ติแฟกต์และสแตทที่แนะนำ

- {exporting ? "Artifacts & Stats" : "อาร์ติแฟกต์และค่าสถานะ"} + Artifacts & Stats

{action} @@ -751,9 +758,9 @@ function ArtifactSectionContent({ {group.name === "Late Game" - ? (exporting ? "Late Game" : "เลทเกม") + ? exportLabel(exporting, "เลทเกม", "Late Game") : group.name === "Early Game" - ? (exporting ? "Early Game" : "ต้นเกม") + ? exportLabel(exporting, "ต้นเกม", "Early Game") : group.name} @@ -806,7 +813,7 @@ function ArtifactSectionContent({ exporting ? ( profile.targetConditions.filter((group) => group.rows.length).map((group, groupIndex) => (
-

{exporting ? "Recommended Minimum Stats" : "ค่าสถานะต่ำๆที่ควรจะปั้นไว้"}

+

{exportLabel(exporting, "ค่าสถานะต่ำๆที่ควรจะปั้นไว้", "Recommended Minimum Stats")}

{group.note ?

{group.note}

: null}
{group.rows.map((target, index) => ( @@ -844,7 +851,7 @@ function ArtifactSectionContent({ group.rows.length).map((group, index) => group.name || `เงื่อนไข ${index + 1}`)}> {profile.targetConditions.filter((group) => group.rows.length).map((group, groupIndex) => (
-

{exporting ? "Recommended Minimum Stats" : "ค่าสถานะต่ำๆที่ควรจะปั้นไว้"}

+

{exportLabel(exporting, "ค่าสถานะต่ำๆที่ควรจะปั้นไว้", "Recommended Minimum Stats")}

{group.note ?

{group.note}

: null}
{group.rows.map((target, index) => ( @@ -865,7 +872,7 @@ function ArtifactSectionContent({ {profile.talents.length ? (

- {exporting ? "Talent Priority" : "ลำดับการอัพพรสวรรค์"} + {exportLabel(exporting, "ลำดับการอัพพรสวรรค์", "Talent Priority")}

{talent.level - ? (exporting ? `Level ${talent.level}+` : `เลเวล ${talent.level}+`) + ? exportLabel(exporting, `เลเวล ${talent.level}+`, `Level ${talent.level}+`) : "-"}

{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")}

{index < talents.length - 1 ? ( @@ -967,7 +974,7 @@ function ArtifactSectionContent({ ) : null} {profile.note ? (
-

{exporting ? "Notes" : "โน็ต"}

+

{exportLabel(exporting, "โน็ต", "Notes")}

{profile.note}

@@ -1016,8 +1023,14 @@ const englishStats: Record = { "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 ( + + ); } function ArtifactStat({ @@ -1028,7 +1041,7 @@ function ArtifactStat({ elementIconUrls, }: { iconSrc: string; - label: string; + label: ReactNode; values: string[]; exporting?: boolean; elementIconUrls: Record; @@ -1143,7 +1156,7 @@ function ConstellationSection({ guide }: { guide: PublicGuide }) { className="size-14 shrink-0 object-contain" />

- กลุ่มดาว + Constellations

{note ? ( @@ -1215,7 +1228,7 @@ function TeamSection({ guide }: { guide: PublicGuide }) {

คำนวณความเสียหาย

-

คำนวณทีม

+

Team Calculation

@@ -1261,7 +1274,7 @@ function ExtraSectionContent({ /> ) : null}
-

{exporting ? "Additional Guide" : "คู่มือเสริม"}

+

คู่มือเสริม

{extra.title}

{action}
@@ -1312,7 +1325,7 @@ function ExtraSectionContent({ ) : null}

{item.name}

- {!item.available ? {exporting ? "Item unavailable" : "ไม่พบรายการ"} : null} + {!item.available ? {exportLabel(exporting, "ไม่พบรายการ", "Item unavailable")} : null}
@@ -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({ เลือกค่าที่ต้องการใช้สร้างภาพเปรียบเทียบอาวุธ +
{metricOptions.map(([metric, label]) => ( + )} /> ))} diff --git a/lib/guides/image-export.test.tsx b/lib/guides/image-export.test.tsx index 1ec2e17..8dd0d77 100644 --- a/lib/guides/image-export.test.tsx +++ b/lib/guides/image-export.test.tsx @@ -2,7 +2,10 @@ import { createElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; -import { GuideImageExport } from "@/components/public/guide-image-export"; +import { + GuideImageExport, + imageExportFileName, +} from "@/components/public/guide-image-export"; import { MaterialPoster } from "@/components/public/material-poster"; import { ConstellationImage } from "@/components/public/constellation-image"; import type { PublicGuide } from "@/lib/guides/queries"; @@ -12,14 +15,16 @@ describe("image export controls", () => { const html = renderToStaticMarkup(No notes); expect(html).toContain("ส่งออกภาพ"); expect(html).toContain("ส่งออกภาพโดยไม่รวมโน้ต"); - expect(html.match(/ { + it("shows only one export action when no-note export is disabled", () => { const html = renderToStaticMarkup(Content); expect(html).toContain("ส่งออกภาพ"); expect(html).not.toContain("ส่งออกภาพโดยไม่รวมโน้ต"); - expect(html.match(/ { @@ -32,12 +37,25 @@ describe("image export controls", () => { const group = { rows: [], note } as unknown as PublicGuide["constellationGroups"][number]; const html = renderToStaticMarkup(createElement(ConstellationImage, { guide, group })); expect(html).toContain("Raiden’s Constellations"); + expect(html).not.toContain("กลุ่มดาวของ Raiden"); expect(html).toContain("Team Damage"); + expect(html).toContain("ดาเมจทั้งทีม"); expect(html).toContain('data-export-note="true"'); expect(html).toContain(note); }); }); +describe("image export filenames", () => { + it("adds the selected language after the note variant", () => { + expect(imageExportFileName("Raiden.png", "th", false)).toBe("Raiden-th.png"); + expect(imageExportFileName("Raiden.png", "en", true)).toBe("Raiden-without-notes-en.png"); + }); + + it("can preserve English-only export filenames", () => { + expect(imageExportFileName("Raiden-materials.png", "th", false, false)).toBe("Raiden-materials.png"); + }); +}); + describe("material poster text", () => { const props = {