diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 14306fb..4444733 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -64,9 +64,9 @@ export default async function RootLayout({ const locale = await getLocale() return ( - + @@ -74,21 +74,24 @@ export default async function RootLayout({ -
+
-
-
diff --git a/src/components/extraSettingBar/index.tsx b/src/components/extraSettingBar/index.tsx index 2d5cde1..cdbf270 100644 --- a/src/components/extraSettingBar/index.tsx +++ b/src/components/extraSettingBar/index.tsx @@ -1,5 +1,5 @@ 'use client' -import { EyeOff, Eye, Hammer, RefreshCw, ShieldBan, User, Swords, SkipForward, BowArrow, Info, RouteIcon, Search, CupSoda, UsersIcon } from "lucide-react" +import { EyeOff, Eye, Hammer, RefreshCw, ShieldBan, User, Swords, SkipForward, BowArrow, Info, RouteIcon, Search, CupSoda, UsersIcon, X, Check, Code, Plus } from "lucide-react" import useGlobalStore from '@/stores/globalStore' import { useTranslations } from "next-intl" import { getLocaleName, getNameChar } from "@/helper" @@ -39,19 +39,19 @@ export default function ExtraSettingBar() { ) const [showSearchStage, setShowSearchStage] = useState(false) const [showSearchLineup, setShowSearchLineup] = useState(false) - const [isChildClick, setIsChildClick] = useState(false) const [stageSearchTerm, setStageSearchTerm] = useState("") + const [lineupSearchTerm, setLineupSearchTerm] = useState("") const [stagePage, setStagePage] = useState(1) const pageSize = 30 const stageList = useMemo(() => Object.values(stage), [stage]) - const term = stageSearchTerm.toLowerCase() + const stageTerm = stageSearchTerm.toLowerCase() const filteredStages = useMemo(() => stageList.filter((s) => - getLocaleName(locale, s.Name).toLowerCase().includes(term) || - String(s.ID).toLowerCase().includes(term) - ), [stageList, term, locale]) + getLocaleName(locale, s.Name).toLowerCase().includes(stageTerm) || + String(s.ID).toLowerCase().includes(stageTerm) + ), [stageList, stageTerm, locale]) const paginatedStages = useMemo(() => filteredStages.slice( (stagePage - 1) * pageSize, @@ -60,164 +60,201 @@ export default function ExtraSettingBar() { const hasMorePages = useMemo(() => stagePage * pageSize < filteredStages.length, [stagePage, filteredStages]) - const onChangeSearch = (v: string) => { + const onChangeStageSearch = (v: string) => { setStageSearchTerm(v) setStagePage(1) } + // Filter avatars for lineup selector + const avatarList = useMemo(() => Object.values(mapAvatar), [mapAvatar]) + const filteredAvatars = useMemo(() => { + if (!lineupSearchTerm.trim()) return avatarList + const term = lineupSearchTerm.toLowerCase() + return avatarList.filter(a => { + const charName = getNameChar(locale, transI18n, a).toLowerCase() + return charName.includes(term) || a.ID.toString().includes(term) + }) + }, [avatarList, lineupSearchTerm, locale, transI18n]) + + const currentLineup = extraData?.theory_craft?.custom_lineup || [] + return ( -
+
{/* THEORY CRAFT */} {extraData?.theory_craft && ( -
-
-

- - {transI18n("theoryCraft")} -

-
- +
+ {/* Section Header */} +
+
+
+ +
+
+
+

+ {transI18n("theoryCraft")} +

+
+ +
+
+

+ {transI18n("detailTheoryCraft")} +

+
+
+ +
+ + { + setExtraData({ + ...extraData, + theory_craft: { + hp: extraData?.theory_craft?.hp || {}, + cycle_count: extraData?.theory_craft?.cycle_count || 1, + mode: e.target.checked, + stage_id: extraData?.theory_craft?.stage_id || 0, + custom_lineup: extraData?.theory_craft?.custom_lineup || [] + } + }) + }} + />
-
- - { - setExtraData({ - ...extraData, - theory_craft: { - hp: extraData?.theory_craft?.hp || {}, - cycle_count: extraData?.theory_craft?.cycle_count || 1, - mode: e.target.checked, - stage_id: extraData?.theory_craft?.stage_id || 0 - } - }) - }} - /> -
- + {/* Theorycraft Expanded Content */} {extraData?.theory_craft?.mode && ( -
- {/* Cycle Count */} -
-
- - {transI18n("cycleCount")} -
- - setExtraData({ - ...extraData, - theory_craft: { - hp: extraData?.theory_craft?.hp || {}, - cycle_count: Number(e.target.value) || 1, - mode: extraData?.theory_craft?.mode || false, - stage_id: extraData?.theory_craft?.stage_id || 0 +
+ {/* Simulation Settings Grid */} +
+ {/* Cycle Count */} +
+ +
+ + setExtraData({ + ...extraData, + theory_craft: { + hp: extraData?.theory_craft?.hp || {}, + cycle_count: Number(e.target.value) || 1, + mode: extraData?.theory_craft?.mode || false, + stage_id: extraData?.theory_craft?.stage_id || 0, + custom_lineup: extraData?.theory_craft?.custom_lineup || [] + } + }) } - }) - } - min="1" - /> -
+ min="1" + /> +
+
- {/* Stage Selection */} -
-
- - {transI18n("stage")} -
-
+ {/* Stage Selection */} +
+ + {/* Stage Search Popover */} {showSearchStage && ( -
e.stopPropagation()} className="absolute top-full mt-2 w-full z-50 bg-base-100 border border-base-content/15 rounded-2xl p-3.5 shadow-2xl space-y-3"> - onChangeSearch(e.target.value)} - autoFocus - /> +
e.stopPropagation()} + className="absolute top-full mt-2 left-0 right-0 z-50 bg-base-100 border border-white/15 rounded-2xl p-3 shadow-2xl space-y-2.5" + > +
+ onChangeStageSearch(e.target.value)} + autoFocus + /> + {stageSearchTerm && ( + + )} +
-
+
{paginatedStages.length > 0 ? ( - paginatedStages.map((stage) => ( -
{ - e.stopPropagation() - setIsChildClick(true) - - if (extraData?.theory_craft?.stage_id !== Number(stage.ID)) { - setExtraData({ - ...extraData, - theory_craft: { - stage_id: Number(stage.ID), - cycle_count: extraData?.theory_craft?.cycle_count ?? 1, - mode: extraData?.theory_craft?.mode ?? false, - hp: extraData?.theory_craft?.hp ?? {}, - custom_lineup: extraData?.theory_craft?.custom_lineup ?? [] - } - }) - } - setShowSearchStage(pre => !pre) - onChangeSearch("") - }} - > - {getLocaleName(locale, stage.Name)} -
- )) + paginatedStages.map((stg) => { + const isSelected = extraData?.theory_craft?.stage_id === Number(stg.ID) + return ( +
{ + if (extraData?.theory_craft?.stage_id !== Number(stg.ID)) { + setExtraData({ + ...extraData, + theory_craft: { + stage_id: Number(stg.ID), + cycle_count: extraData?.theory_craft?.cycle_count ?? 1, + mode: extraData?.theory_craft?.mode ?? false, + hp: extraData?.theory_craft?.hp ?? {}, + custom_lineup: extraData?.theory_craft?.custom_lineup ?? [] + } + }) + } + setShowSearchStage(false) + onChangeStageSearch("") + }} + > + {getLocaleName(locale, stg.Name)} + {isSelected && } +
+ ) + }) ) : (
{transI18n("noStageFound")}
)}
{paginatedStages.length > 0 && ( -
+
- Page {stagePage} + Page {stagePage} @@ -229,23 +266,47 @@ export default function ExtraSettingBar() {
{/* Custom Lineup */} -
-
- - {transI18n("customLineup")} +
+
+ + +
-
- {(extraData?.theory_craft?.custom_lineup || []).map((avatarId) => { - const avatarData = mapAvatar[avatarId]; - if (!avatarData) return null; + {/* Selected Avatars Strip */} +
+ {currentLineup.map((avatarId) => { + const avatarData = mapAvatar[avatarId] + if (!avatarData) return null return ( { - const newLineup = (extraData?.theory_craft?.custom_lineup || []).filter(id => id !== avatarId); + const newLineup = currentLineup.filter(id => id !== avatarId) setExtraData({ ...extraData, theory_craft: { @@ -255,51 +316,56 @@ export default function ExtraSettingBar() { hp: extraData?.theory_craft?.hp ?? {}, custom_lineup: newLineup } - }); + }) }} /> - ); + ) })} - + {!showSearchLineup && ( + + )}
+ {/* Inline Character Selection Pool */} {showSearchLineup && ( -
e.stopPropagation()} - className="mt-2 w-full z-50 bg-base-100 border border-base-content/15 rounded-2xl p-4 shadow-2xl space-y-3" - > -
- {transI18n("selectedCharacters")} - +
+
+ + setLineupSearchTerm(e.target.value)} + autoFocus + />
-
- {Object.values(mapAvatar).map((avatar) => { - const currentLineup = extraData?.theory_craft?.custom_lineup || []; - const isSelected = currentLineup.includes(avatar.ID.toString()); - +
+ {filteredAvatars.map((avatar) => { + const isSelected = currentLineup.includes(avatar.ID.toString()) return ( { - const idStr = avatar.ID.toString(); - let newLineup = [...currentLineup]; + const idStr = avatar.ID.toString() + let newLineup = [...currentLineup] if (isSelected) { - newLineup = newLineup.filter(id => id !== idStr); + newLineup = newLineup.filter(id => id !== idStr) } else { - newLineup.push(idStr); + newLineup.push(idStr) } setExtraData({ @@ -311,10 +377,10 @@ export default function ExtraSettingBar() { hp: extraData?.theory_craft?.hp ?? {}, custom_lineup: newLineup } - }); + }) }} /> - ); + ) })}
@@ -322,187 +388,237 @@ export default function ExtraSettingBar() {
)} -
+
)} - {/* MULTIPATH CHAR */} -
-
-
-

- {transI18n("multipathCharacter")} -

-
- + {/* MULTIPATH CHARACTER */} +
+
+
+
+ +
+
+
+

+ {transI18n("multipathCharacter")} +

+
+ +
+
+

+ {transI18n("detailMultipathCharacter")} +

- setIsEnableChangePath(e.target.checked)} - /> + +
+ + setIsEnableChangePath(e.target.checked)} + /> +
{isEnableChangePath && ( -
-
-
- - {transI18n("mainPath")} +
+
+ {/* Main Character Path */} +
+ + extraData?.multi_path?.multi_path_main?.includes(Number(it.ID))).map((it) => ({ + value: it.ID.toString(), + label: getNameChar(locale, transI18n, it), + imageUrl: `${process.env.CDN_URL}/${baseType?.[it.BaseType]?.Icon}` + }))} + excludeSet={[]} + selectedCustomSet={extraData?.multi_path?.main?.toString() || ""} + placeholder={transI18n("selectAMainStat")} + setSelectedCustomSet={(it) => { + if (!it) return + setExtraData({ + ...extraData, + multi_path: { + march_7: extraData?.multi_path?.march_7 || 1001, + main: Number(it) || 8001, + multi_path_main: extraData?.multi_path?.multi_path_main || [], + multi_path_march_7: extraData?.multi_path?.multi_path_march_7 || [] + }, + lua: extraData?.lua || null + }) + }} + />
- extraData?.multi_path?.multi_path_main?.includes(Number(it.ID))).map((it) => ({ - value: it.ID.toString(), - label: getNameChar(locale, transI18n, it), - imageUrl: `${process.env.CDN_URL}/${baseType?.[it.BaseType]?.Icon}` - }))} - excludeSet={[]} - selectedCustomSet={extraData?.multi_path?.main?.toString() || ""} - placeholder={transI18n("selectAMainStat")} - setSelectedCustomSet={(it) => { - if (!it) return - setExtraData({ - ...extraData, - multi_path: { - march_7: extraData?.multi_path?.march_7 || 1001, - main: Number(it) || 8001, - multi_path_main: extraData?.multi_path?.multi_path_main || [], - multi_path_march_7: extraData?.multi_path?.multi_path_march_7 || [] - }, - lua: extraData?.lua || null - }) - }} - /> -
-
-
- - {transI18n("march7Path")} + {/* March 7th Path */} +
+ + extraData?.multi_path?.multi_path_march_7?.includes(Number(it.ID))).map((it) => ({ + value: it.ID.toString(), + label: getNameChar(locale, transI18n, it), + imageUrl: `${process.env.CDN_URL}/${baseType?.[it.BaseType]?.Icon}` + }))} + excludeSet={[]} + selectedCustomSet={extraData?.multi_path?.march_7?.toString() || ""} + placeholder={transI18n("selectAMainStat")} + setSelectedCustomSet={(it) => { + if (!it) return + setExtraData({ + ...extraData, + multi_path: { + march_7: Number(it) || 1001, + main: extraData?.multi_path?.main || 8001, + multi_path_main: extraData?.multi_path?.multi_path_main || [], + multi_path_march_7: extraData?.multi_path?.multi_path_march_7 || [] + }, + lua: extraData?.lua || null + }) + }} + />
- extraData?.multi_path?.multi_path_march_7?.includes(Number(it.ID))).map((it) => ({ - value: it.ID.toString(), - label: getNameChar(locale, transI18n, it), - imageUrl: `${process.env.CDN_URL}/${baseType?.[it.BaseType]?.Icon}` - }))} - excludeSet={[]} - selectedCustomSet={extraData?.multi_path?.march_7?.toString() || ""} - placeholder={transI18n("selectAMainStat")} - setSelectedCustomSet={(it) => { - if (!it) return - setExtraData({ - ...extraData, - multi_path: { - march_7: Number(it) || 1001, - main: extraData?.multi_path?.main || 8001, - multi_path_main: extraData?.multi_path?.multi_path_main || [], - multi_path_march_7: extraData?.multi_path?.multi_path_march_7 || [] - }, - lua: extraData?.lua || null - }) - }} - />
)} -
+
- {/* CHALLENGE */} -
-
-

- {transI18n("challenge")} -

-
- -
-
-
-
- - {transI18n("anomalyArbitration")} -
-
- -
-
- + {/* CHALLENGE SETTINGS */} +
+
+
+
- -
-
-
- - {transI18n("skipNode")} -
-
- -
-
- +
+

+ {transI18n("challenge")} +

+

+ Configure anomaly peak mode and node skip settings +

-
+ +
+
+ {/* Anomaly Arbitration */} +
+
+ +
+ +
+
+ +
+ + {/* Skip Node */} +
+
+ +
+ +
+
+ +
+
+
+
{/* EXTRA FEATURES */} -
-
-

- {transI18n("extraFeatures")} -

+
+
+
+ +
+
+

+ {transI18n("extraFeatures")} +

+

+ UI display overrides and visual preferences +

+
-
+
+ {/* Hide UI Row */} {extraData?.setting?.hide_ui !== undefined && ( -
-
- {extraData?.setting?.hide_ui - ? - : - } - {transI18n("hideUI")} -
- +
+
+
+ {extraData?.setting?.hide_ui + ? + : + } +
+
+
+ {transI18n("hideUI")} +
+ +
+
+

+ {transI18n("detailHiddenUi")} +

@@ -525,13 +641,23 @@ export default function ExtraSettingBar() {
)} + {/* Disable Censorship Row */} {extraData?.setting?.censorship !== undefined && ( -
-
- - {transI18n("disableCensorship")} -
- +
+
+
+ +
+
+
+ {transI18n("disableCensorship")} +
+ +
+
+

+ {transI18n("detailDisableCensorship")} +

@@ -554,25 +680,60 @@ export default function ExtraSettingBar() {
)}
-
+
- {/* LUA EDITOR */} -
-
-
- -

Lua Scripting

+ {/* LUA SCRIPTING */} +
+
+
+
+ +
+
+

+ Lua Scripting +

+

+ Inject custom Lua runtime scripts and behaviors +

+
+
+ +
+ + setIsEnableLua(e.target.checked)} + />
- setIsEnableLua(e.target.checked)} - />
{isEnableLua && ( -
+
+ {/* Terminal Window Header Bar */} +
+
+
+
+
+
+
+
+
+ + script.lua +
+
+ + LUA 5.4 + +
+ + {/* Code Editor */} @@ -584,15 +745,15 @@ export default function ExtraSettingBar() { highlight={(code) => Prism.highlight(code, Prism.languages.lua, "lua")} padding={16} textareaClassName="outline-none font-mono" - className="text-xs font-mono min-h-60 bg-[#161822] text-white" + className="text-xs font-mono min-h-60 text-slate-100 bg-[#0d0e15]" style={{ - background: "#161822", + background: "#0d0e15", color: "#e2e8f0", }} />
)} -
+
) } diff --git a/src/components/header/index.tsx b/src/components/header/index.tsx index bbcdc97..46144e8 100644 --- a/src/components/header/index.tsx +++ b/src/components/header/index.tsx @@ -636,7 +636,15 @@ export default function Header() { {/* Modals */} {modalConfigs?.map(({ id, title, onClose, content }) => ( -
+
{/* Modal Header */}

diff --git a/src/components/importBar/enka.tsx b/src/components/importBar/enka.tsx index ec1cebf..030613e 100644 --- a/src/components/importBar/enka.tsx +++ b/src/components/importBar/enka.tsx @@ -167,85 +167,83 @@ export default function EnkaImport() { }; return ( -
-
- {/* Header */} -
-

HSR UID

+
+ {/* Header */} +
+

HSR UID

-
- setUidInput(e.target.value)} - className="input input-bordered w-full max-w-xs rounded-xl bg-base-200 border-base-300 text-sm focus:border-primary" - placeholder="Enter UID" - /> - + {selectedCharacters.length > 0 && ( + - {selectedCharacters.length > 0 && ( - - )} -
- {Error && ( -
😭 {Error}
)} - - {/* Player Info */} - {enkaData && ( -
-
Name: {enkaData.detailInfo.nickname}
-
UID: {enkaData.detailInfo.uid}
-
Level: {enkaData.detailInfo.level}
-
- )} - - {/* Selection Info */} -
-
- - {transI18n("selectedCharacters")}: - - - {selectedCharacters.length} - -
-
- - -
-
- {isLoading && ( -
-
+ {Error && ( +
😭 {Error}
+ )} + + {/* Player Info */} + {enkaData && ( +
+
Name: {enkaData.detailInfo.nickname}
+
UID: {enkaData.detailInfo.uid}
+
Level: {enkaData.detailInfo.level}
)} - {/* Character Grid */} -
- {validCharacters.map((character) => ( - - ))} + + {/* Selection Info */} +
+
+ + {transI18n("selectedCharacters")}: + + + {selectedCharacters.length} + +
+
+ + +
+ {isLoading && ( +
+
+
+ )} + {/* Character Grid */} +
+ {validCharacters.map((character) => ( + + ))} +
); } diff --git a/src/components/importBar/freesr.tsx b/src/components/importBar/freesr.tsx index 4312099..992b219 100644 --- a/src/components/importBar/freesr.tsx +++ b/src/components/importBar/freesr.tsx @@ -189,74 +189,72 @@ export default function FreeSRImport() { } return ( -
-
- {/* Header */} -
-

{transI18n("freeSRImport")}

+
+ {/* Header */} +
+

{transI18n("freeSRImport")}

-
-
- {transI18n("pickAFile")} - - -
+
+
+ {transI18n("pickAFile")} + + +
- {selectedCharacters.length > 0 && ( - - )} -
- {Error && ( -
😭 {Error}
+ {selectedCharacters.length > 0 && ( + )} - - {/* Selection Info */} -
-
- - {transI18n("selectedCharacters")}: - - - {selectedCharacters.length} - -
-
- - -
-
- {isLoading && ( -
-
-
+ {Error && ( +
😭 {Error}
)} - {/* Character Grid */} -
- {validFreeSRData && Object.values(validFreeSRData.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => { - return ( - - ) - })} + + {/* Selection Info */} +
+
+ + {transI18n("selectedCharacters")}: + + + {selectedCharacters.length} + +
+
+ + +
+ {isLoading && ( +
+
+
+ )} + {/* Character Grid */} +
+ {validFreeSRData && Object.values(validFreeSRData.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => { + return ( + + ) + })} +
) } diff --git a/src/components/showcaseCard/index.tsx b/src/components/showcaseCard/index.tsx index 4e1978e..d1acaaf 100644 --- a/src/components/showcaseCard/index.tsx +++ b/src/components/showcaseCard/index.tsx @@ -15,6 +15,7 @@ import useDetailDataStore from '@/stores/detailDataStore'; import useCurrentDataStore from '@/stores/currentDataStore'; import { getLocaleName } from '@/helper'; import { useShallow } from 'zustand/react/shallow'; +import { Copy, Download, Image as ImageIcon } from 'lucide-react'; export default function ShowCaseInfo() { @@ -36,6 +37,8 @@ export default function ShowCaseInfo() { useShallow((state) => ({ avatars: state.avatars })) ) const [avgColor, setAvgColor] = useState('#222'); + const [isSaving, setIsSaving] = useState(false); + const [isCopyingImage, setIsCopyingImage] = useState(false); const imgRef = useRef(null); const cardRef = useRef(null) const { locale } = useLocaleStore( @@ -43,31 +46,56 @@ export default function ShowCaseInfo() { ) const transI18n = useTranslations("DataPage") - const handleSaveImage = useCallback(() => { + const handleSaveImage = useCallback(async () => { if (cardRef.current === null || !avatarSelected) { toast.error("Avatar showcase not found!"); return; } + if (isSaving) return; - import("html2canvas-pro") - .then(({ default: html2canvas }) => - html2canvas(cardRef.current!, { - scale: 2, - backgroundColor: "#000000", - allowTaint: false, - useCORS: true - }) - ) - .then((canvas: HTMLCanvasElement) => { + setIsSaving(true); + try { + const { default: html2canvas } = await import("html2canvas-pro"); + const canvas = await html2canvas(cardRef.current!, { + scale: 2, + backgroundColor: "#000000", + allowTaint: false, + useCORS: true, + logging: false, + width: 1600, + height: 650, + windowWidth: 1600, + windowHeight: 650, + onclone: (clonedDoc) => { + const clonedCard = clonedDoc.querySelector('[data-showcase-card="true"]') as HTMLElement | null; + if (clonedCard) { + clonedCard.style.transform = "none"; + clonedCard.style.width = "1600px"; + clonedCard.style.minHeight = "650px"; + } + } + }); + + canvas.toBlob((blob) => { + if (!blob) { + toast.error("Failed to generate image!"); + setIsSaving(false); + return; + } + const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.download = `${getNameChar(locale, transI18n, avatarSelected)}_showcase.png`; - link.href = canvas.toDataURL("image/png"); + link.href = url; link.click(); - }) - .catch(() => { - toast.error("Error generating showcase card!"); - }); - }, [avatarSelected, locale, transI18n]); + URL.revokeObjectURL(url); + setIsSaving(false); + toast.success(locale === "vi" ? "Đã lưu ảnh thành công!" : "Image saved successfully!"); + }, "image/png"); + } catch { + toast.error("Error generating showcase card!"); + setIsSaving(false); + } + }, [avatarSelected, locale, transI18n, isSaving]); useEffect(() => { if (!avatarSelected) return; @@ -512,6 +540,163 @@ export default function ShowCaseInfo() { return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`; }, []) + const wrapperRef = useRef(null); + const [scale, setScale] = useState(1); + + useEffect(() => { + if (!wrapperRef.current) return; + const updateScale = () => { + if (wrapperRef.current) { + const containerWidth = wrapperRef.current.clientWidth; + const baseWidth = 1600; + const newScale = Math.min(1, containerWidth / baseWidth); + setScale(newScale); + } + }; + + updateScale(); + const resizeObserver = new ResizeObserver(updateScale); + resizeObserver.observe(wrapperRef.current); + return () => resizeObserver.disconnect(); + }, []); + + const handleCopyBuildText = useCallback(() => { + if (!avatarSelected || !avatarData) { + toast.error("Avatar not found!"); + return; + } + + const charName = getNameChar(locale, transI18n, avatarSelected); + const charLevel = `Lv. ${avatarData.level}/80`; + const eidolonRank = `E${avatarData.data?.rank ?? 0}`; + + // Light cone info + let lcText = "None"; + if (avatarProfile?.lightcone && mapLightCone?.[avatarProfile.lightcone.item_id]) { + const lcData = mapLightCone[avatarProfile.lightcone.item_id]; + const lcName = getLocaleName(locale, lcData.Name) || lcData.Name?.en || "Light Cone"; + const lcRank = `S${avatarProfile.lightcone.rank || 1}`; + const lcLevel = `Lv. ${avatarProfile.lightcone.level || 80}/80`; + lcText = `${lcName} (${lcRank}, ${lcLevel})`; + } + + // Stats summary + const statsList: string[] = []; + if (characterStats) { + const mainStatKeys = ["HP", "ATK", "DEF", "SPD", "CRITRate", "CRITDmg", "BreakEffect", "EnergyRate", "EffectHitRate", "EffectRES"]; + for (const key of mainStatKeys) { + const stat = characterStats[key]; + if (stat) { + const valFormatted = stat.round === 0 + ? Math.round(stat.value).toLocaleString() + : stat.value.toFixed(stat.round); + statsList.push(`• ${stat.name}: ${valFormatted}${stat.unit}`); + } + } + } + + // Relic sets + const relicSetList: string[] = []; + if (relicEffects && relicEffects.length > 0) { + for (const effect of relicEffects) { + const relicSetData = mapRelicSet?.[effect.key]; + if (relicSetData) { + const setName = getLocaleName(locale, relicSetData.Name) || relicSetData.Name?.en || "Relic Set"; + relicSetList.push(`• ${setName} (${effect.count}pc)`); + } + } + } + + // Individual Relics (Slots 1-6) + const relicSlotNames = ["Head", "Hands", "Body", "Feet", "Planar Sphere", "Link Rope"]; + const relicDetails: string[] = []; + if (relicStats && relicStats.length > 0) { + relicStats.forEach((r, idx) => { + if (!r) return; + const slotName = relicSlotNames[idx] || `Slot ${idx + 1}`; + const mainStatStr = r.mainAffix ? `${r.mainAffix.detail?.name || ""}: +${r.mainAffix.valueAffix}${r.mainAffix.detail?.unit || ""}` : ""; + const subAffixesStr = r.subAffix?.filter(Boolean).map(s => `${s?.detail?.name || ""}+${s?.valueAffix}${s?.detail?.unit || ""}`).join(", ") || ""; + relicDetails.push(` - ${slotName}: ${mainStatStr} | Subs: ${subAffixesStr}`); + }); + } + + const lines = [ + `=== ${charName} (${eidolonRank} | ${charLevel}) ===`, + `Light Cone: ${lcText}`, + ``, + `[Attributes]`, + ...statsList, + ``, + `[Relic Sets]`, + relicSetList.length > 0 ? relicSetList.join("\n") : "None", + ]; + + if (relicDetails.length > 0) { + lines.push(``, `[Relic Details]`, ...relicDetails); + } + + const textToCopy = lines.join("\n"); + navigator.clipboard.writeText(textToCopy) + .then(() => { + toast.success(locale === "vi" ? "Đã sao chép thông tin build!" : "Build info copied to clipboard!"); + }) + .catch(() => { + toast.error(locale === "vi" ? "Lỗi khi sao chép!" : "Failed to copy build info!"); + }); + }, [avatarSelected, avatarData, avatarProfile, mapLightCone, locale, transI18n, characterStats, relicEffects, mapRelicSet, relicStats]); + + const handleCopyImage = useCallback(async () => { + if (cardRef.current === null || !avatarSelected) { + toast.error("Avatar showcase not found!"); + return; + } + if (isCopyingImage) return; + + setIsCopyingImage(true); + try { + const { default: html2canvas } = await import("html2canvas-pro"); + const canvas = await html2canvas(cardRef.current!, { + scale: 2, + backgroundColor: "#000000", + allowTaint: false, + useCORS: true, + logging: false, + width: 1600, + height: 650, + windowWidth: 1600, + windowHeight: 650, + onclone: (clonedDoc) => { + const clonedCard = clonedDoc.querySelector('[data-showcase-card="true"]') as HTMLElement | null; + if (clonedCard) { + clonedCard.style.transform = "none"; + clonedCard.style.width = "1600px"; + clonedCard.style.minHeight = "650px"; + } + } + }); + + canvas.toBlob((blob) => { + if (!blob) { + toast.error("Failed to create image blob"); + setIsCopyingImage(false); + return; + } + navigator.clipboard.write([ + new ClipboardItem({ "image/png": blob }) + ]).then(() => { + toast.success(locale === "vi" ? "Đã sao chép ảnh vào Clipboard!" : "Image copied to clipboard!"); + }).catch(() => { + toast.error(locale === "vi" ? "Trình duyệt không cho phép sao chép ảnh trực tiếp!" : "Clipboard write failed!"); + }).finally(() => { + setIsCopyingImage(false); + }); + }, "image/png"); + } catch { + toast.error("Error generating showcase image!"); + setIsCopyingImage(false); + } + }, [avatarSelected, locale, isCopyingImage]); + return (
@@ -523,27 +708,71 @@ export default function ShowCaseInfo() { {transI18n("showcaseCard")}

- +
+ + + +
-
+
-
- -
+
+
+ +
-
+
); }