Files
Firefly_Srtools/src/components/avatarInfo/index.tsx
T
2026-08-28 19:23:41 +07:00

480 lines
31 KiB
TypeScript

/* eslint-disable react-hooks/exhaustive-deps */
"use client"
import useUserDataStore from "@/stores/userDataStore";
import { useEffect, useMemo } from "react";
import LightconeBar from '../lightconeBar'
import { calcPromotion, calcRarity, replaceByParam } from '@/helper';
import { getSkillTree } from '@/helper/getSkillTree';
import { useTranslations } from 'next-intl';
import ParseText from '../parseText';
import useLocaleStore from '@/stores/localeStore';
import useModelStore from '@/stores/modelStore';
import Image from 'next/image';
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { getLocaleName } from "@/helper/getName";
import { useShallow } from "zustand/react/shallow";
export default function AvatarInfo() {
const { avatarSelected, setResetDataLightcone } = useCurrentDataStore(
useShallow((state) => ({
avatarSelected: state.avatarSelected,
setResetDataLightcone: state.setResetDataLightcone,
}))
)
const { avatars, setAvatars, setAvatar } = useUserDataStore(
useShallow((state) => ({
avatars: state.avatars,
setAvatars: state.setAvatars,
setAvatar: state.setAvatar,
}))
)
const { isOpenLightcone, setIsOpenLightcone } = useModelStore(
useShallow((state) => ({
isOpenLightcone: state.isOpenLightcone,
setIsOpenLightcone: state.setIsOpenLightcone,
}))
)
const { mapLightCone, baseType } = useDetailDataStore(
useShallow((state) => ({
mapLightCone: state.mapLightCone,
baseType: state.baseType,
}))
)
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
);
const lightcone = useMemo(() => {
if (!avatarSelected) return null;
const avatar = avatars[avatarSelected.ID];
return avatar?.profileList?.[avatar?.profileSelect]?.lightcone ?? avatar?.profileList?.[0]?.lightcone ?? null;
}, [avatarSelected, avatars]);
const handleUpdateLightcone = (updater: (lc: typeof lightcone) => typeof lightcone) => {
if (!avatarSelected || !avatars[avatarSelected.ID]) return;
const avatar = avatars[avatarSelected.ID];
const profileIndex = avatar.profileSelect;
const currentProfile = avatar.profileList?.[profileIndex];
if (!currentProfile) return;
const newProfileList = [...avatar.profileList];
newProfileList[profileIndex] = {
...currentProfile,
lightcone: updater(currentProfile.lightcone),
};
setAvatar({
...avatar,
profileList: newProfileList,
});
};
const lightconeDetail = useMemo(() => {
if (!mapLightCone || !lightcone?.item_id) return null;
return mapLightCone?.[lightcone.item_id.toString()] || null;
}, [lightcone, mapLightCone]);
const handleShow = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
setIsOpenLightcone(true);
modal.showModal();
}
};
// Close modal handler
const handleCloseModal = (modalId: string) => {
setIsOpenLightcone(false);
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.close();
}
};
// Handle ESC key to close modal
useEffect(() => {
if (!isOpenLightcone) {
handleCloseModal("action_detail_modal");
return;
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpenLightcone) {
handleCloseModal("action_detail_modal");
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenLightcone]);
return (
<div className="w-full space-y-4">
{avatarSelected && avatars[avatarSelected?.ID.toString() || ""] && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6 w-full items-start">
{/* Left Column: Character Configuration */}
<div className="glass-card rounded-2xl p-5 sm:p-6 border border-base-content/10 shadow-xs flex flex-col justify-between">
<div>
{/* Card Header */}
<div className="flex items-center justify-between pb-4 mb-2 border-b border-base-content/10">
<h2 className="text-base sm:text-lg font-bold flex items-center gap-2.5">
<div className="w-1.5 h-5 bg-linear-to-b from-primary to-accent rounded-full"></div>
<span className="bg-clip-text text-transparent bg-linear-to-r from-primary to-accent font-bold tracking-tight">
{transI18n("characterSettings")}
</span>
</h2>
<span className="text-xs font-mono font-semibold px-2.5 py-1 rounded-md bg-primary/10 text-primary border border-primary/20">
ID: #{avatarSelected.ID}
</span>
</div>
<div className="divide-y divide-base-content/10">
{/* Level Control */}
<div className="py-4 space-y-2.5">
<div className="flex items-center justify-between">
<span className="text-sm font-bold text-base-content tracking-tight flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-primary" />
{transI18n("characterLevel")}
</span>
<span className="text-xs font-mono font-bold px-2.5 py-1 rounded-md bg-primary/15 text-primary border border-primary/25">
{avatars[avatarSelected?.ID.toString() || ""]?.level} / 80
</span>
</div>
<div className="flex items-center gap-3 pt-0.5">
<input
type="range"
min="1"
max="80"
value={avatars[avatarSelected?.ID.toString() || ""]?.level}
onChange={(e) => {
const newLevel = Math.min(80, Math.max(1, parseInt(e.target.value) || 1));
setAvatars({ ...avatars, [avatarSelected?.ID.toString() || ""]: { ...avatars[avatarSelected?.ID.toString() || ""], level: newLevel, promotion: calcPromotion(newLevel) } });
}}
className="range range-primary range-sm flex-1"
/>
<button
onClick={() => {
setAvatars({ ...avatars, [avatarSelected?.ID.toString() || ""]: { ...avatars[avatarSelected?.ID.toString() || ""], level: 80, promotion: calcPromotion(80) } });
}}
className="btn btn-sm btn-primary btn-outline h-8 min-h-8 rounded-lg font-bold px-3"
>
{transI18n("max")}
</button>
</div>
</div>
{/* Ultimate Energy Control */}
<div className="py-4 space-y-2.5">
<div className="flex items-center justify-between">
<span className="text-sm font-bold text-base-content tracking-tight flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-amber-400" />
{transI18n("ultimateEnergy")}
</span>
<div className="flex items-center gap-2">
<span className="text-xs font-mono font-bold text-amber-500">
{Math.round(avatars[avatarSelected?.ID.toString() || ""]?.sp_value)} / {avatars[avatarSelected?.ID.toString() || ""]?.sp_max}
</span>
<span className="text-xs font-mono text-base-content/70 font-semibold">
({((avatars[avatarSelected?.ID.toString() || ""]?.sp_value / avatars[avatarSelected?.ID.toString() || ""]?.sp_max) * 100).toFixed(0)}%)
</span>
</div>
</div>
<div className="flex items-center gap-3 pt-0.5">
<input
type="range"
min="0"
max={avatars[avatarSelected?.ID.toString() || ""]?.sp_max}
value={avatars[avatarSelected?.ID.toString() || ""]?.sp_value}
onChange={(e) => {
if (!avatars[avatarSelected?.ID.toString() || ""]?.can_change_sp) return
const newSpValue = Math.min(avatars[avatarSelected?.ID.toString() || ""]?.sp_max, Math.max(0, parseInt(e.target.value) || 0));
setAvatars({ ...avatars, [avatarSelected?.ID.toString() || ""]: { ...avatars[avatarSelected?.ID.toString() || ""], sp_value: newSpValue } });
}}
className="range range-warning range-sm flex-1"
/>
<button
className="btn btn-sm btn-warning btn-outline h-8 min-h-8 font-bold px-3 rounded-lg shadow-xs active-press"
onClick={() => {
if (!avatars[avatarSelected?.ID.toString() || ""]?.can_change_sp) return
const newSpValue = Math.ceil(avatars[avatarSelected?.ID.toString() || ""]?.sp_max / 2);
const newAvatar = { ...avatars[avatarSelected?.ID.toString() || ""], sp_value: newSpValue }
setAvatar(newAvatar)
}}
>
{transI18n("setTo50")}
</button>
</div>
</div>
{/* Technique Toggle */}
<div className="py-4 flex items-center justify-between gap-4">
<div className="space-y-1">
<span className="text-sm font-bold text-base-content tracking-tight flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-secondary" />
{transI18n("useTechnique")}
</span>
<p className="text-xs text-base-content/70 font-medium pl-3">{transI18n("techniqueNote")}</p>
</div>
<input
type="checkbox"
checked={avatars[avatarSelected?.ID.toString() || ""]?.techniques.length > 0}
onChange={(e) => {
if (!avatarSelected || avatarSelected?.MazeBuff?.length == 0) return
const techniques = e.target.checked ? avatarSelected?.MazeBuff : [];
const newAvatar = { ...avatars[avatarSelected?.ID.toString() || ""], techniques: techniques };
setAvatar(newAvatar);
}}
className="toggle toggle-primary"
/>
</div>
{/* Enhancement Selection */}
{avatarSelected?.Enhanced && (
<div className="py-4 space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-bold text-base-content tracking-tight flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-accent" />
{transI18n("enhancement")}
</span>
<span className="text-xs font-mono text-secondary font-bold">
{avatars[avatarSelected?.ID.toString() || ""]?.enhanced || transI18n("origin")}
</span>
</div>
<select
value={avatars[avatarSelected?.ID.toString() || ""]?.enhanced || ""}
onChange={(e) => {
const currentAvatar = avatars[avatarSelected?.ID.toString() || ""]
if (currentAvatar && avatarSelected) {
const skillTree = getSkillTree(avatarSelected, e.target.value)
const newAvatar = {
...currentAvatar,
enhanced: e.target.value,
data: {
...currentAvatar.data,
skills: skillTree || currentAvatar.data.skills
}
}
setAvatar(newAvatar)
}
}}
className="select select-sm select-bordered w-full rounded-xl bg-base-200/50"
>
<option value="">{transI18n("origin")}</option>
{Object.keys(avatarSelected?.Enhanced || {}).map((key) => (
<option key={key} value={key}>
{key}
</option>
))}
</select>
<p className="text-xs text-base-content/60 font-medium">{transI18n("enhancedNote")}</p>
</div>
)}
</div>
</div>
</div>
{/* Right Column: Lightcone Equipment */}
<div className="glass-card rounded-2xl p-5 sm:p-6 border border-base-content/10 shadow-xs flex flex-col justify-between">
<div>
{/* Card Header */}
<div className="flex items-center justify-between pb-4 mb-2 border-b border-base-content/10">
<h2 className="text-base sm:text-lg font-bold flex items-center gap-2.5">
<div className="w-1.5 h-5 bg-linear-to-b from-secondary to-primary rounded-full"></div>
<span className="bg-clip-text text-transparent bg-linear-to-r from-secondary to-primary font-bold tracking-tight">
{transI18n("lightconeEquipment")}
</span>
</h2>
{lightcone && (
<span className="text-xs font-mono font-semibold px-2.5 py-1 rounded-md bg-secondary/10 text-secondary border border-secondary/20">
S{lightcone.rank} • Lv.{lightcone.level}
</span>
)}
</div>
{lightcone && lightconeDetail ? (
<div className="space-y-4">
{/* Lightcone Hero Block */}
<div className="grid grid-cols-1 sm:grid-cols-12 gap-4 items-center">
{/* Artwork */}
<div className="sm:col-span-4 aspect-3/4 rounded-xl overflow-hidden bg-base-200/60 flex items-center justify-center border border-base-content/10 shadow-xs">
<Image
unoptimized
crossOrigin="anonymous"
width={904}
height={1260}
priority
src={`${process.env.CDN_URL}/${lightconeDetail?.Image?.ImagePath}`}
className="w-full h-full object-cover"
alt="Lightcone"
/>
</div>
{/* Meta & Stats */}
<div className="sm:col-span-8 space-y-3">
<div>
<h3 className="text-base sm:text-lg font-bold tracking-tight text-base-content">
<ParseText
locale={locale}
text={getLocaleName(locale, lightconeDetail.Name)}
/>
</h3>
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
<span className="text-xs px-2.5 py-0.5 rounded-md bg-base-200 font-semibold text-base-content/80 border border-base-content/10">
{transI18n(lightconeDetail.BaseType.toLowerCase())}
</span>
<span className="text-xs px-2.5 py-0.5 rounded-md bg-amber-500/15 text-amber-500 font-bold border border-amber-500/25">
{calcRarity(lightconeDetail.Rarity)}★
</span>
<span className="text-xs font-mono text-base-content/60 font-medium">#{lightcone.item_id}</span>
</div>
</div>
{/* Level Slider */}
<div className="space-y-1.5">
<div className="flex items-center justify-between text-xs font-semibold">
<span className="text-base-content/80">{transI18n("level")}</span>
<span className="font-mono text-primary font-bold">{lightcone.level}/80</span>
</div>
<input
type="range"
min="1"
max="80"
value={lightcone.level || 1}
onChange={(e) => {
const newLightconeLevel = Math.min(80, Math.max(1, parseInt(e.target.value) || 1))
handleUpdateLightcone((lc) => lc ? { ...lc, level: newLightconeLevel, promotion: calcPromotion(newLightconeLevel) } : null)
}}
className="range range-primary range-sm w-full"
/>
</div>
{/* Superimposition Rank (S1-S5) */}
<div className="space-y-1.5">
<div className="flex items-center justify-between text-xs font-semibold">
<span className="text-base-content/80">{transI18n("superimpositionRank")}</span>
<span className="font-mono text-info font-bold">S{lightcone.rank}</span>
</div>
<div className="grid grid-cols-5 gap-1.5">
{[1, 2, 3, 4, 5].map((r) => (
<button
key={r}
onClick={() => {
handleUpdateLightcone((lc) => lc ? { ...lc, rank: r } : null)
}}
className={`btn btn-xs rounded-lg font-mono font-bold transition-all ${
lightcone.rank === r
? 'btn-primary shadow-xs'
: 'btn-ghost bg-base-200 hover:bg-base-300 text-base-content/80'
}`}
>
S{r}
</button>
))}
</div>
</div>
</div>
</div>
{/* Skill Description */}
<div className="p-3.5 rounded-xl bg-base-200/50 border border-base-content/10">
<div
className="text-xs sm:text-sm text-base-content/90 leading-relaxed font-normal"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, lightconeDetail.Skills.Desc),
lightconeDetail.Skills.Level[lightcone.rank.toString()]?.Param || []
)
}}
/>
</div>
{/* Action Buttons */}
<div className="flex items-center gap-3 pt-2">
<button
onClick={() => {
if (avatarSelected) {
setResetDataLightcone(avatarSelected, baseType)
handleShow("action_detail_modal")
}
}}
className="btn btn-primary btn-sm flex-1 rounded-xl font-bold shadow-xs"
>
{transI18n("changeLightcone")}
</button>
<button
onClick={() => {
handleUpdateLightcone(() => null)
}}
className="btn btn-error btn-outline hover:btn-error btn-sm rounded-xl font-bold px-3.5 gap-1.5 transition-all"
title={transI18n("removeLightcone")}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
<span className="text-xs">{transI18n("removeLightcone")}</span>
</button>
</div>
</div>
) : (
/* Empty Lightcone State */
<div className="text-center py-8 space-y-3.5">
<div
onClick={() => handleShow("action_detail_modal")}
className="w-16 h-16 mx-auto bg-base-200/70 rounded-2xl border-2 border-dashed border-base-content/30 flex items-center justify-center cursor-pointer hover:border-primary hover:scale-105 transition-all group"
>
<svg className="w-7 h-7 text-base-content/50 group-hover:text-primary transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
</div>
<div>
<h3 className="text-sm font-bold text-base-content">{transI18n("noLightconeEquipped")}</h3>
<p className="text-xs text-base-content/60 font-medium mt-0.5">{transI18n("equipLightconeNote")}</p>
</div>
<button
onClick={() => {
if (avatarSelected) {
setResetDataLightcone(avatarSelected, baseType)
handleShow("action_detail_modal")
}
}}
className="btn btn-primary btn-sm rounded-xl font-bold px-6 shadow-xs"
>
{transI18n("equipLightcone")}
</button>
</div>
)}
</div>
</div>
</div>
)}
{/* Lightcone Selection Modal */}
<dialog id="action_detail_modal" className="modal">
<div className="modal-box w-11/12 max-w-[92vw] sm:max-w-[88vw] max-h-[90vh] flex flex-col bg-base-100 text-base-content border border-white/10 shadow-2xl rounded-2xl p-0 overflow-hidden">
<div className="sticky top-0 z-20 flex items-center justify-between px-6 py-4 border-b border-base-content/10 bg-base-100 shrink-0">
<h3 className="font-bold text-xl tracking-tight text-base-content">
{transI18n("lightconeEquipment")}
</h3>
<button
className="btn btn-sm btn-circle btn-ghost text-base-content/60 hover:text-base-content hover:bg-base-200 transition-colors"
onClick={() => handleCloseModal("action_detail_modal")}
>
✕
</button>
</div>
<div className="p-4 sm:p-6 overflow-y-auto flex-1">
<LightconeBar />
</div>
</div>
<form method="dialog" className="modal-backdrop">
<button onClick={() => handleCloseModal("action_detail_modal")}>close</button>
</form>
</dialog>
</div>
);
}