Refactor TypeScript interfaces and types for consistency and readability
Gitea Auto Deploy / Deploy-Container (push) Successful in 49s
Gitea Auto Deploy / Deploy-Container (push) Successful in 49s
- Reformatted multiple TypeScript interfaces across various files to ensure consistent spacing and indentation. - Updated `changelog.ts`, `enka.ts`, `extraData.ts`, `gloval.d.ts`, `index.ts`, `lightconeDetail.ts`, `metaData.ts`, `mics.ts`, `modelConfig.ts`, `monsterDetail.ts`, `peakDetail.ts`, `pfDetail.ts`, `psConnect.ts`, `relicDetail.ts`, `showcase.ts`, `srtools.ts`, and `zod/index.ts` for improved code clarity. - Ensured all interfaces are properly formatted with consistent line breaks and spacing. - Made minor adjustments to the `tsconfig.json` file for formatting consistency.
This commit is contained in:
+444
-444
@@ -1,445 +1,445 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from 'next/navigation'
|
||||
import ParseText from "../parseText";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import { getNameChar } from "@/helper/getName";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import useModelStore from "@/stores/modelStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { ModalConfig, RelicStore } from "@/types";
|
||||
import { toast } from "react-toastify";
|
||||
import useGlobalStore from "@/stores/globalStore";
|
||||
import { connectToPS, syncDataToPS } from "@/helper";
|
||||
import CopyImport from "../importBar/copy";
|
||||
import useCopyProfileStore from "@/stores/copyProfile";
|
||||
import AvatarBar from "../avatarBar";
|
||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
|
||||
export default function ActionBar() {
|
||||
const router = useRouter()
|
||||
const { avatarSelected } = useCurrentDataStore()
|
||||
const { damageType, baseType } = useDetailDataStore()
|
||||
const { setResetData } = useCopyProfileStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { locale } = useLocaleStore()
|
||||
const {
|
||||
isOpenCreateProfile,
|
||||
setIsOpenCreateProfile,
|
||||
isOpenCopy,
|
||||
setIsOpenCopy,
|
||||
isOpenAvatars,
|
||||
setIsOpenAvatars
|
||||
} = useModelStore()
|
||||
const { avatars, setAvatar } = useUserDataStore()
|
||||
const [profileName, setProfileName] = useState("");
|
||||
const [formState, setFormState] = useState("EDIT");
|
||||
const [profileEdit, setProfileEdit] = useState(-1);
|
||||
const { isConnectPS } = useGlobalStore()
|
||||
|
||||
const profileCurrent = useMemo(() => {
|
||||
if (!avatarSelected) return null;
|
||||
const avatar = avatars[avatarSelected.ID];
|
||||
return avatar?.profileList[avatar.profileSelect] || null;
|
||||
}, [avatarSelected, avatars]);
|
||||
|
||||
const listProfile = useMemo(() => {
|
||||
if (!avatarSelected) return [];
|
||||
const avatar = avatars[avatarSelected.ID];
|
||||
return avatar?.profileList || [];
|
||||
}, [avatarSelected, avatars]);
|
||||
|
||||
|
||||
const handleUpdateProfile = () => {
|
||||
if (!profileName.trim()) return;
|
||||
if (formState === "CREATE" && avatarSelected && avatars[avatarSelected.ID]) {
|
||||
const newListProfile = [...listProfile]
|
||||
const newProfile = {
|
||||
profile_name: profileName,
|
||||
lightcone: null,
|
||||
relics: {} as Record<string, RelicStore>
|
||||
}
|
||||
newListProfile.push(newProfile)
|
||||
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: newListProfile.length - 1 })
|
||||
toast.success("Profile created successfully")
|
||||
} else if (formState === "EDIT" && profileCurrent && avatarSelected && profileEdit !== -1) {
|
||||
const newListProfile = [...listProfile]
|
||||
newListProfile[profileEdit].profile_name = profileName;
|
||||
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile })
|
||||
toast.success("Profile updated successfully")
|
||||
}
|
||||
handleCloseModal("update_profile_modal");
|
||||
};
|
||||
|
||||
|
||||
const handleShow = (modalId: string) => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
modal.showModal();
|
||||
}
|
||||
};
|
||||
|
||||
// Close modal handler
|
||||
const handleCloseModal = (modalId: string) => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
modal.close();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const actionMove = (path: string) => {
|
||||
router.push(`/${path}`)
|
||||
}
|
||||
|
||||
const handleProfileSelect = (profileId: number) => {
|
||||
if (!avatarSelected) return;
|
||||
if (avatars[avatarSelected.ID].profileSelect === profileId) return;
|
||||
setAvatar({ ...avatars[avatarSelected.ID], profileSelect: profileId })
|
||||
toast.success(`Profile changed to Profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}`)
|
||||
}
|
||||
|
||||
const handleDeleteProfile = (profileId: number, e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!avatarSelected || profileId == 0) return;
|
||||
if (window.confirm(`Are you sure you want to delete profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}?`)) {
|
||||
const newListProfile = [...listProfile]
|
||||
newListProfile.splice(profileId, 1)
|
||||
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: profileId - 1 })
|
||||
toast.success(`Profile ${avatars[avatarSelected.ID].profileList[profileId].profile_name} deleted successfully`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const handleConnectOrSyncPS = async () => {
|
||||
if (isConnectPS) {
|
||||
const res = await syncDataToPS()
|
||||
if (res.success) {
|
||||
toast.success(transI18n("syncSuccess"))
|
||||
} else {
|
||||
toast.error(`${transI18n("syncFailed")}: ${res.message}`)
|
||||
}
|
||||
} else {
|
||||
const res = await connectToPS()
|
||||
if (res.success) {
|
||||
toast.success(transI18n("connectedSuccess"))
|
||||
} else {
|
||||
toast.error(`${transI18n("connectedFailed")}: ${res.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const modalConfigs: ModalConfig[] = [
|
||||
{
|
||||
id: "update_profile_modal",
|
||||
title: formState === "CREATE" ? transI18n("createNewProfile") : transI18n("editProfile"),
|
||||
isOpen: isOpenCreateProfile,
|
||||
onClose: () => {
|
||||
setIsOpenCreateProfile(false)
|
||||
handleCloseModal("update_profile_modal")
|
||||
},
|
||||
content: (
|
||||
<div className="px-6 space-y-4">
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text text-primary font-semibold text-lg">
|
||||
{transI18n("profileName")}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={transI18n("placeholderProfileName")}
|
||||
className="input input-warning mt-1 w-full"
|
||||
value={profileName}
|
||||
onChange={(e) => setProfileName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="modal-action">
|
||||
<button className="btn btn-success btn-sm sm:btn-md" onClick={handleUpdateProfile}>
|
||||
{formState === "CREATE" ? transI18n("create") : transI18n("update")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: "copy_profile_modal",
|
||||
title: transI18n("copyProfiles").toUpperCase(),
|
||||
isOpen: isOpenCopy,
|
||||
onClose: () => {
|
||||
setIsOpenCopy(false)
|
||||
handleCloseModal("copy_profile_modal")
|
||||
},
|
||||
content: <CopyImport />
|
||||
},
|
||||
{
|
||||
id: "avatars_modal",
|
||||
title: transI18n("avatars").toUpperCase(),
|
||||
isOpen: isOpenAvatars,
|
||||
onClose: () => {
|
||||
setIsOpenAvatars(false)
|
||||
handleCloseModal("avatars_modal")
|
||||
},
|
||||
content: <AvatarBar onClose={() => { setIsOpenAvatars(false); handleCloseModal("avatars_modal") }} />
|
||||
}
|
||||
]
|
||||
|
||||
// Handle ESC key to close modal
|
||||
useEffect(() => {
|
||||
for (const item of modalConfigs) {
|
||||
if (!item?.isOpen) {
|
||||
handleCloseModal(item?.id || "")
|
||||
}
|
||||
}
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
for (const item of modalConfigs) {
|
||||
handleCloseModal(item?.id || "")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleEscKey);
|
||||
return () => window.removeEventListener('keydown', handleEscKey);
|
||||
}, [isOpenCopy, isOpenCreateProfile, isOpenAvatars]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full px-4 pb-4 bg-base-200">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 items-center justify-items-center">
|
||||
|
||||
<div className="flex flex-col justify-center w-full">
|
||||
<div className="flex flex-wrap items-center gap-2 ">
|
||||
<div className="flex flex-wrap items-center h-full opacity-80 lg:hover:opacity-100 cursor-pointer text-base md:text-lg lg:text-xl">
|
||||
{avatarSelected && (
|
||||
<div className="flex flex-wrap items-center justify-start h-full w-full">
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/${damageType?.[avatarSelected?.DamageType]?.Icon}`}
|
||||
alt={'damage type'}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
className="h-10 w-10 object-contain"
|
||||
width={100}
|
||||
height={100}
|
||||
/>
|
||||
<p className="text-center font-bold text-lg">
|
||||
{transI18n(avatarSelected.BaseType.toLowerCase())}
|
||||
</p>
|
||||
<div className="text-center font-bold text-lg">{" / "}</div>
|
||||
<ParseText
|
||||
locale={locale}
|
||||
text={getNameChar(locale, transI18n, avatarSelected).toWellFormed()}
|
||||
className={"font-bold text-lg"}
|
||||
/>
|
||||
<div className="text-center italic text-sm ml-2"> {`(${avatarSelected.ID})`}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-base opacity-70 font-bold w-16">{transI18n("profile")}:</span>
|
||||
<div className="dropdown dropdown-center md:dropdown-start">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="btn btn-warning border-info btn-soft gap-1"
|
||||
>
|
||||
<span className="truncate max-w-48 font-bold">
|
||||
{profileCurrent?.profile_name}
|
||||
</span>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<ul className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box min-w-max w-full mt-1 border border-base-300 max-h-60 overflow-y-auto">
|
||||
{listProfile.map((profile, index) => (
|
||||
<li key={index} className="grid grid-cols-12">
|
||||
<button
|
||||
className={`col-span-8 btn btn-ghost`}
|
||||
onClick={() => handleProfileSelect(index)}
|
||||
>
|
||||
<span className="flex-1 truncate text-left">
|
||||
{profile.profile_name}
|
||||
{index === 0 && (
|
||||
<span className="text-xs text-base-content/60 ml-1">({transI18n("default")})</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
</button>
|
||||
{index !== 0 && (
|
||||
<>
|
||||
<button
|
||||
className="col-span-2 flex items-center justify-center text-lg text-warning hover:bg-warning/20 z-20 w-full h-full"
|
||||
onClick={() => {
|
||||
setFormState("EDIT");
|
||||
setProfileName(profile.profile_name)
|
||||
setProfileEdit(index)
|
||||
setIsOpenCreateProfile(true)
|
||||
handleShow("update_profile_modal")
|
||||
}}
|
||||
title="Edit Profile"
|
||||
>
|
||||
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536M4 20h4l10.293-10.293a1 1 0 000-1.414l-2.586-2.586a1 1 0 00-1.414 0L4 16v4z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="col-span-2 flex items-center justify-center text-error hover:bg-error/20 z-20 w-full h-full text-lg"
|
||||
onClick={(e) => {
|
||||
handleDeleteProfile(index, e)
|
||||
}}
|
||||
title="Delete Profile"
|
||||
>
|
||||
<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-6v6M9 7V4h6v3m5 0H4" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
|
||||
<li className="border-t border-base-300 mt-2 pt-2 z-10">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsOpenCopy(true)
|
||||
setResetData(baseType, damageType)
|
||||
handleShow("copy_profile_modal")
|
||||
}}
|
||||
className="btn btn-ghost flex justify-start px-3 py-2 h-full w-full hover:bg-base-200 cursor-pointer text-primary z-20"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
{transI18n("copyProfiles")}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-ghost flex justify-start px-3 py-2 h-full w-full hover:bg-base-200 cursor-pointer text-primary z-20"
|
||||
onClick={() => {
|
||||
|
||||
setIsOpenCreateProfile(true)
|
||||
setFormState("CREATE");
|
||||
setProfileName("")
|
||||
handleShow("update_profile_modal")
|
||||
}}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
{transI18n("addNewProfile")}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className=" grid grid-cols-2 w-full sm:hidden gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsOpenAvatars(true)
|
||||
handleShow("avatars_modal")
|
||||
}}
|
||||
className="col-span-1 btn btn-warning btn-sm w-full">
|
||||
{transI18n("avatars")}
|
||||
</button>
|
||||
<div className="col-span-1 dropdown dropdown-center w-full">
|
||||
<label tabIndex={0} className="btn btn-info btn-sm w-full">
|
||||
{transI18n("actions")}
|
||||
</label>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box min-w-max w-full mt-1 border border-base-300 max-h-60 overflow-y-auto"
|
||||
>
|
||||
<li>
|
||||
<button onClick={() => actionMove('')}>
|
||||
{transI18n("characterInformation")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => actionMove('relics-info')}>
|
||||
{transI18n("relics")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => actionMove('eidolons-info')}>
|
||||
{transI18n("eidolons")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => actionMove('skills-info')}>
|
||||
{transI18n("skills")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => actionMove('showcase-card')}>
|
||||
{transI18n("showcaseCard")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
|
||||
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:grid grid-cols-3 gap-2 w-full">
|
||||
<button className="btn btn-success btn-sm" onClick={() => actionMove("")}>
|
||||
{transI18n("characterInformation")}
|
||||
</button>
|
||||
<button className="btn btn-success btn-sm" onClick={() => actionMove("relics-info")}>
|
||||
{transI18n("relics")}
|
||||
</button>
|
||||
<button className="btn btn-success btn-sm" onClick={() => actionMove("eidolons-info")}>
|
||||
{transI18n("eidolons")}
|
||||
</button>
|
||||
<button className="btn btn-success btn-sm" onClick={() => actionMove("skills-info")}>
|
||||
{transI18n("skills")}
|
||||
</button>
|
||||
<button className="btn btn-success btn-sm" onClick={() => actionMove("showcase-card")}>
|
||||
{transI18n("showcaseCard")}
|
||||
</button>
|
||||
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
|
||||
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{modalConfigs.map(({ id, title, onClose, content }) => (
|
||||
<dialog key={id} id={id} className="modal">
|
||||
<div className="modal-box w-11/12 max-w-[90%] max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
|
||||
<div className="sticky top-0 z-10">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1, rotate: 90 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
|
||||
onClick={onClose}
|
||||
>
|
||||
✕
|
||||
</motion.button>
|
||||
</div>
|
||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
{content}
|
||||
</div>
|
||||
</dialog>
|
||||
))}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
"use client";
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from 'next/navigation'
|
||||
import ParseText from "../parseText";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import { getNameChar } from "@/helper/getName";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import useModelStore from "@/stores/modelStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { ModalConfig, RelicStore } from "@/types";
|
||||
import { toast } from "react-toastify";
|
||||
import useGlobalStore from "@/stores/globalStore";
|
||||
import { connectToPS, syncDataToPS } from "@/helper";
|
||||
import CopyImport from "../importBar/copy";
|
||||
import useCopyProfileStore from "@/stores/copyProfile";
|
||||
import AvatarBar from "../avatarBar";
|
||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
|
||||
export default function ActionBar() {
|
||||
const router = useRouter()
|
||||
const { avatarSelected } = useCurrentDataStore()
|
||||
const { damageType, baseType } = useDetailDataStore()
|
||||
const { setResetData } = useCopyProfileStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { locale } = useLocaleStore()
|
||||
const {
|
||||
isOpenCreateProfile,
|
||||
setIsOpenCreateProfile,
|
||||
isOpenCopy,
|
||||
setIsOpenCopy,
|
||||
isOpenAvatars,
|
||||
setIsOpenAvatars
|
||||
} = useModelStore()
|
||||
const { avatars, setAvatar } = useUserDataStore()
|
||||
const [profileName, setProfileName] = useState("");
|
||||
const [formState, setFormState] = useState("EDIT");
|
||||
const [profileEdit, setProfileEdit] = useState(-1);
|
||||
const { isConnectPS } = useGlobalStore()
|
||||
|
||||
const profileCurrent = useMemo(() => {
|
||||
if (!avatarSelected) return null;
|
||||
const avatar = avatars[avatarSelected.ID];
|
||||
return avatar?.profileList[avatar.profileSelect] || null;
|
||||
}, [avatarSelected, avatars]);
|
||||
|
||||
const listProfile = useMemo(() => {
|
||||
if (!avatarSelected) return [];
|
||||
const avatar = avatars[avatarSelected.ID];
|
||||
return avatar?.profileList || [];
|
||||
}, [avatarSelected, avatars]);
|
||||
|
||||
|
||||
const handleUpdateProfile = () => {
|
||||
if (!profileName.trim()) return;
|
||||
if (formState === "CREATE" && avatarSelected && avatars[avatarSelected.ID]) {
|
||||
const newListProfile = [...listProfile]
|
||||
const newProfile = {
|
||||
profile_name: profileName,
|
||||
lightcone: null,
|
||||
relics: {} as Record<string, RelicStore>
|
||||
}
|
||||
newListProfile.push(newProfile)
|
||||
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: newListProfile.length - 1 })
|
||||
toast.success("Profile created successfully")
|
||||
} else if (formState === "EDIT" && profileCurrent && avatarSelected && profileEdit !== -1) {
|
||||
const newListProfile = [...listProfile]
|
||||
newListProfile[profileEdit].profile_name = profileName;
|
||||
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile })
|
||||
toast.success("Profile updated successfully")
|
||||
}
|
||||
handleCloseModal("update_profile_modal");
|
||||
};
|
||||
|
||||
|
||||
const handleShow = (modalId: string) => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
modal.showModal();
|
||||
}
|
||||
};
|
||||
|
||||
// Close modal handler
|
||||
const handleCloseModal = (modalId: string) => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
modal.close();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const actionMove = (path: string) => {
|
||||
router.push(`/${path}`)
|
||||
}
|
||||
|
||||
const handleProfileSelect = (profileId: number) => {
|
||||
if (!avatarSelected) return;
|
||||
if (avatars[avatarSelected.ID].profileSelect === profileId) return;
|
||||
setAvatar({ ...avatars[avatarSelected.ID], profileSelect: profileId })
|
||||
toast.success(`Profile changed to Profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}`)
|
||||
}
|
||||
|
||||
const handleDeleteProfile = (profileId: number, e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (!avatarSelected || profileId == 0) return;
|
||||
if (window.confirm(`Are you sure you want to delete profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}?`)) {
|
||||
const newListProfile = [...listProfile]
|
||||
newListProfile.splice(profileId, 1)
|
||||
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: profileId - 1 })
|
||||
toast.success(`Profile ${avatars[avatarSelected.ID].profileList[profileId].profile_name} deleted successfully`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const handleConnectOrSyncPS = async () => {
|
||||
if (isConnectPS) {
|
||||
const res = await syncDataToPS()
|
||||
if (res.success) {
|
||||
toast.success(transI18n("syncSuccess"))
|
||||
} else {
|
||||
toast.error(`${transI18n("syncFailed")}: ${res.message}`)
|
||||
}
|
||||
} else {
|
||||
const res = await connectToPS()
|
||||
if (res.success) {
|
||||
toast.success(transI18n("connectedSuccess"))
|
||||
} else {
|
||||
toast.error(`${transI18n("connectedFailed")}: ${res.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const modalConfigs: ModalConfig[] = [
|
||||
{
|
||||
id: "update_profile_modal",
|
||||
title: formState === "CREATE" ? transI18n("createNewProfile") : transI18n("editProfile"),
|
||||
isOpen: isOpenCreateProfile,
|
||||
onClose: () => {
|
||||
setIsOpenCreateProfile(false)
|
||||
handleCloseModal("update_profile_modal")
|
||||
},
|
||||
content: (
|
||||
<div className="px-6 space-y-4">
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text text-primary font-semibold text-lg">
|
||||
{transI18n("profileName")}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={transI18n("placeholderProfileName")}
|
||||
className="input input-warning mt-1 w-full"
|
||||
value={profileName}
|
||||
onChange={(e) => setProfileName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="modal-action">
|
||||
<button className="btn btn-success btn-sm sm:btn-md" onClick={handleUpdateProfile}>
|
||||
{formState === "CREATE" ? transI18n("create") : transI18n("update")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
id: "copy_profile_modal",
|
||||
title: transI18n("copyProfiles").toUpperCase(),
|
||||
isOpen: isOpenCopy,
|
||||
onClose: () => {
|
||||
setIsOpenCopy(false)
|
||||
handleCloseModal("copy_profile_modal")
|
||||
},
|
||||
content: <CopyImport />
|
||||
},
|
||||
{
|
||||
id: "avatars_modal",
|
||||
title: transI18n("avatars").toUpperCase(),
|
||||
isOpen: isOpenAvatars,
|
||||
onClose: () => {
|
||||
setIsOpenAvatars(false)
|
||||
handleCloseModal("avatars_modal")
|
||||
},
|
||||
content: <AvatarBar onClose={() => { setIsOpenAvatars(false); handleCloseModal("avatars_modal") }} />
|
||||
}
|
||||
]
|
||||
|
||||
// Handle ESC key to close modal
|
||||
useEffect(() => {
|
||||
for (const item of modalConfigs) {
|
||||
if (!item?.isOpen) {
|
||||
handleCloseModal(item?.id || "")
|
||||
}
|
||||
}
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
for (const item of modalConfigs) {
|
||||
handleCloseModal(item?.id || "")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleEscKey);
|
||||
return () => window.removeEventListener('keydown', handleEscKey);
|
||||
}, [isOpenCopy, isOpenCreateProfile, isOpenAvatars]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full px-4 pb-4 bg-base-200">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 items-center justify-items-center">
|
||||
|
||||
<div className="flex flex-col justify-center w-full">
|
||||
<div className="flex flex-wrap items-center gap-2 ">
|
||||
<div className="flex flex-wrap items-center h-full opacity-80 lg:hover:opacity-100 cursor-pointer text-base md:text-lg lg:text-xl">
|
||||
{avatarSelected && (
|
||||
<div className="flex flex-wrap items-center justify-start h-full w-full">
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/${damageType?.[avatarSelected?.DamageType]?.Icon}`}
|
||||
alt={'damage type'}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
className="h-10 w-10 object-contain"
|
||||
width={100}
|
||||
height={100}
|
||||
/>
|
||||
<p className="text-center font-bold text-lg">
|
||||
{transI18n(avatarSelected.BaseType.toLowerCase())}
|
||||
</p>
|
||||
<div className="text-center font-bold text-lg">{" / "}</div>
|
||||
<ParseText
|
||||
locale={locale}
|
||||
text={getNameChar(locale, transI18n, avatarSelected).toWellFormed()}
|
||||
className={"font-bold text-lg"}
|
||||
/>
|
||||
<div className="text-center italic text-sm ml-2"> {`(${avatarSelected.ID})`}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-base opacity-70 font-bold w-16">{transI18n("profile")}:</span>
|
||||
<div className="dropdown dropdown-center md:dropdown-start">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="btn btn-warning border-info btn-soft gap-1"
|
||||
>
|
||||
<span className="truncate max-w-48 font-bold">
|
||||
{profileCurrent?.profile_name}
|
||||
</span>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<ul className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box min-w-max w-full mt-1 border border-base-300 max-h-60 overflow-y-auto">
|
||||
{listProfile.map((profile, index) => (
|
||||
<li key={index} className="grid grid-cols-12">
|
||||
<button
|
||||
className={`col-span-8 btn btn-ghost`}
|
||||
onClick={() => handleProfileSelect(index)}
|
||||
>
|
||||
<span className="flex-1 truncate text-left">
|
||||
{profile.profile_name}
|
||||
{index === 0 && (
|
||||
<span className="text-xs text-base-content/60 ml-1">({transI18n("default")})</span>
|
||||
)}
|
||||
</span>
|
||||
|
||||
</button>
|
||||
{index !== 0 && (
|
||||
<>
|
||||
<button
|
||||
className="col-span-2 flex items-center justify-center text-lg text-warning hover:bg-warning/20 z-20 w-full h-full"
|
||||
onClick={() => {
|
||||
setFormState("EDIT");
|
||||
setProfileName(profile.profile_name)
|
||||
setProfileEdit(index)
|
||||
setIsOpenCreateProfile(true)
|
||||
handleShow("update_profile_modal")
|
||||
}}
|
||||
title="Edit Profile"
|
||||
>
|
||||
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536M4 20h4l10.293-10.293a1 1 0 000-1.414l-2.586-2.586a1 1 0 00-1.414 0L4 16v4z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="col-span-2 flex items-center justify-center text-error hover:bg-error/20 z-20 w-full h-full text-lg"
|
||||
onClick={(e) => {
|
||||
handleDeleteProfile(index, e)
|
||||
}}
|
||||
title="Delete Profile"
|
||||
>
|
||||
<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-6v6M9 7V4h6v3m5 0H4" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
|
||||
<li className="border-t border-base-300 mt-2 pt-2 z-10">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsOpenCopy(true)
|
||||
setResetData(baseType, damageType)
|
||||
handleShow("copy_profile_modal")
|
||||
}}
|
||||
className="btn btn-ghost flex justify-start px-3 py-2 h-full w-full hover:bg-base-200 cursor-pointer text-primary z-20"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
{transI18n("copyProfiles")}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-ghost flex justify-start px-3 py-2 h-full w-full hover:bg-base-200 cursor-pointer text-primary z-20"
|
||||
onClick={() => {
|
||||
|
||||
setIsOpenCreateProfile(true)
|
||||
setFormState("CREATE");
|
||||
setProfileName("")
|
||||
handleShow("update_profile_modal")
|
||||
}}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
{transI18n("addNewProfile")}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className=" grid grid-cols-2 w-full sm:hidden gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsOpenAvatars(true)
|
||||
handleShow("avatars_modal")
|
||||
}}
|
||||
className="col-span-1 btn btn-warning btn-sm w-full">
|
||||
{transI18n("avatars")}
|
||||
</button>
|
||||
<div className="col-span-1 dropdown dropdown-center w-full">
|
||||
<label tabIndex={0} className="btn btn-info btn-sm w-full">
|
||||
{transI18n("actions")}
|
||||
</label>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box min-w-max w-full mt-1 border border-base-300 max-h-60 overflow-y-auto"
|
||||
>
|
||||
<li>
|
||||
<button onClick={() => actionMove('')}>
|
||||
{transI18n("characterInformation")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => actionMove('relics-info')}>
|
||||
{transI18n("relics")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => actionMove('eidolons-info')}>
|
||||
{transI18n("eidolons")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => actionMove('skills-info')}>
|
||||
{transI18n("skills")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => actionMove('showcase-card')}>
|
||||
{transI18n("showcaseCard")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
|
||||
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden sm:grid grid-cols-3 gap-2 w-full">
|
||||
<button className="btn btn-success btn-sm" onClick={() => actionMove("")}>
|
||||
{transI18n("characterInformation")}
|
||||
</button>
|
||||
<button className="btn btn-success btn-sm" onClick={() => actionMove("relics-info")}>
|
||||
{transI18n("relics")}
|
||||
</button>
|
||||
<button className="btn btn-success btn-sm" onClick={() => actionMove("eidolons-info")}>
|
||||
{transI18n("eidolons")}
|
||||
</button>
|
||||
<button className="btn btn-success btn-sm" onClick={() => actionMove("skills-info")}>
|
||||
{transI18n("skills")}
|
||||
</button>
|
||||
<button className="btn btn-success btn-sm" onClick={() => actionMove("showcase-card")}>
|
||||
{transI18n("showcaseCard")}
|
||||
</button>
|
||||
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
|
||||
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{modalConfigs.map(({ id, title, onClose, content }) => (
|
||||
<dialog key={id} id={id} className="modal">
|
||||
<div className="modal-box w-11/12 max-w-[90%] max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
|
||||
<div className="sticky top-0 z-10">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1, rotate: 90 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
|
||||
onClick={onClose}
|
||||
>
|
||||
✕
|
||||
</motion.button>
|
||||
</div>
|
||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
{content}
|
||||
</div>
|
||||
</dialog>
|
||||
))}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+140
-140
@@ -1,140 +1,140 @@
|
||||
"use client"
|
||||
import Image from "next/image"
|
||||
import { useMemo } from "react"
|
||||
import CharacterCard from "../card/characterCard"
|
||||
import useLocaleStore from "@/stores/localeStore"
|
||||
import { useTranslations } from "next-intl"
|
||||
import useDetailDataStore from "@/stores/detailDataStore"
|
||||
import useCurrentDataStore from '@/stores/currentDataStore';
|
||||
import { calcRarity, getNameChar } from "@/helper"
|
||||
|
||||
export default function AvatarBar({ onClose }: { onClose?: () => void }) {
|
||||
const {
|
||||
avatarSearch,
|
||||
mapAvatarElementActive,
|
||||
mapAvatarPathActive,
|
||||
setAvatarSearch,
|
||||
setAvatarSelected,
|
||||
setMapAvatarElementActive,
|
||||
setMapAvatarPathActive,
|
||||
setSkillIDSelected,
|
||||
} = useCurrentDataStore()
|
||||
const { mapAvatar, baseType, damageType } = useDetailDataStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const {locale} = useLocaleStore()
|
||||
|
||||
const listAvatar = useMemo(() => {
|
||||
if (!mapAvatar || !locale || !transI18n) return []
|
||||
|
||||
let list = Object.values(mapAvatar)
|
||||
|
||||
if (avatarSearch) {
|
||||
list = list.filter(item =>
|
||||
getNameChar(locale, transI18n, item)
|
||||
.toLowerCase()
|
||||
.includes(avatarSearch.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
const allElementFalse = !Object.values(mapAvatarElementActive).some(v => v)
|
||||
const allPathFalse = !Object.values(mapAvatarPathActive).some(v => v)
|
||||
|
||||
list = list.filter(item =>
|
||||
(allElementFalse || mapAvatarElementActive[item.DamageType]) &&
|
||||
(allPathFalse || mapAvatarPathActive[item.BaseType])
|
||||
)
|
||||
|
||||
list.sort((a, b) => {
|
||||
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
|
||||
if (r !== 0) return r
|
||||
return b.ID - a.ID
|
||||
})
|
||||
|
||||
return list
|
||||
}, [mapAvatar, mapAvatarElementActive, mapAvatarPathActive, avatarSearch, locale, transI18n])
|
||||
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row h-full auto-rows-max w-full">
|
||||
<div className="h-full rounded-lg mx-2 py-2">
|
||||
<div className="container">
|
||||
<div className="flex flex-col h-full gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-center">
|
||||
<input type="text"
|
||||
placeholder={transI18n("placeholderCharacter")}
|
||||
className="input input-bordered input-primary w-full"
|
||||
value={avatarSearch}
|
||||
onChange={(e) => setAvatarSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-7 sm:grid-cols-4 lg:grid-cols-7 mb-1 mx-1 gap-2 w-full max-h-[17vh] min-h-[5vh] overflow-y-auto">
|
||||
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => {
|
||||
setMapAvatarElementActive({ ...mapAvatarElementActive, [key]: !mapAvatarElementActive[key] })
|
||||
}}
|
||||
className="hover:bg-gray-600 grid items-center justify-items-center cursor-pointer rounded-md shadow-lg"
|
||||
style={{
|
||||
backgroundColor: mapAvatarElementActive[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||
alt={key}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
|
||||
width={200}
|
||||
height={200} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-9 sm:grid-cols-5 lg:grid-cols-9 mb-1 mx-1 gap-2 overflow-y-auto w-full max-h-[17vh] min-h-[5vh]">
|
||||
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => {
|
||||
setMapAvatarPathActive({ ...mapAvatarPathActive, [key]: !mapAvatarPathActive[key] })
|
||||
}}
|
||||
className="hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-lg cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: mapAvatarPathActive[key] ? "#374151" : "#6B7280"
|
||||
}}
|
||||
>
|
||||
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
alt={key}
|
||||
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
|
||||
width={200}
|
||||
height={200} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start h-full">
|
||||
<ul className="grid grid-cols-3 sm:grid-cols-2 lg:grid-cols-3 gap-2 w-full h-[65vh] overflow-y-scroll overflow-x-hidden">
|
||||
{listAvatar.map((item, index) => (
|
||||
<div key={index} onClick={() => {
|
||||
setAvatarSelected(item);
|
||||
setSkillIDSelected(null)
|
||||
if (onClose) onClose()
|
||||
}}>
|
||||
<CharacterCard data={item} />
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
"use client"
|
||||
import Image from "next/image"
|
||||
import { useMemo } from "react"
|
||||
import CharacterCard from "../card/characterCard"
|
||||
import useLocaleStore from "@/stores/localeStore"
|
||||
import { useTranslations } from "next-intl"
|
||||
import useDetailDataStore from "@/stores/detailDataStore"
|
||||
import useCurrentDataStore from '@/stores/currentDataStore';
|
||||
import { calcRarity, getNameChar } from "@/helper"
|
||||
|
||||
export default function AvatarBar({ onClose }: { onClose?: () => void }) {
|
||||
const {
|
||||
avatarSearch,
|
||||
mapAvatarElementActive,
|
||||
mapAvatarPathActive,
|
||||
setAvatarSearch,
|
||||
setAvatarSelected,
|
||||
setMapAvatarElementActive,
|
||||
setMapAvatarPathActive,
|
||||
setSkillIDSelected,
|
||||
} = useCurrentDataStore()
|
||||
const { mapAvatar, baseType, damageType } = useDetailDataStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const {locale} = useLocaleStore()
|
||||
|
||||
const listAvatar = useMemo(() => {
|
||||
if (!mapAvatar || !locale || !transI18n) return []
|
||||
|
||||
let list = Object.values(mapAvatar)
|
||||
|
||||
if (avatarSearch) {
|
||||
list = list.filter(item =>
|
||||
getNameChar(locale, transI18n, item)
|
||||
.toLowerCase()
|
||||
.includes(avatarSearch.toLowerCase())
|
||||
)
|
||||
}
|
||||
|
||||
const allElementFalse = !Object.values(mapAvatarElementActive).some(v => v)
|
||||
const allPathFalse = !Object.values(mapAvatarPathActive).some(v => v)
|
||||
|
||||
list = list.filter(item =>
|
||||
(allElementFalse || mapAvatarElementActive[item.DamageType]) &&
|
||||
(allPathFalse || mapAvatarPathActive[item.BaseType])
|
||||
)
|
||||
|
||||
list.sort((a, b) => {
|
||||
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
|
||||
if (r !== 0) return r
|
||||
return b.ID - a.ID
|
||||
})
|
||||
|
||||
return list
|
||||
}, [mapAvatar, mapAvatarElementActive, mapAvatarPathActive, avatarSearch, locale, transI18n])
|
||||
|
||||
|
||||
return (
|
||||
<div className="grid grid-flow-row h-full auto-rows-max w-full">
|
||||
<div className="h-full rounded-lg mx-2 py-2">
|
||||
<div className="container">
|
||||
<div className="flex flex-col h-full gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex justify-center">
|
||||
<input type="text"
|
||||
placeholder={transI18n("placeholderCharacter")}
|
||||
className="input input-bordered input-primary w-full"
|
||||
value={avatarSearch}
|
||||
onChange={(e) => setAvatarSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-7 sm:grid-cols-4 lg:grid-cols-7 mb-1 mx-1 gap-2 w-full max-h-[17vh] min-h-[5vh] overflow-y-auto">
|
||||
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => {
|
||||
setMapAvatarElementActive({ ...mapAvatarElementActive, [key]: !mapAvatarElementActive[key] })
|
||||
}}
|
||||
className="hover:bg-gray-600 grid items-center justify-items-center cursor-pointer rounded-md shadow-lg"
|
||||
style={{
|
||||
backgroundColor: mapAvatarElementActive[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||
alt={key}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
|
||||
width={200}
|
||||
height={200} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-9 sm:grid-cols-5 lg:grid-cols-9 mb-1 mx-1 gap-2 overflow-y-auto w-full max-h-[17vh] min-h-[5vh]">
|
||||
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => {
|
||||
setMapAvatarPathActive({ ...mapAvatarPathActive, [key]: !mapAvatarPathActive[key] })
|
||||
}}
|
||||
className="hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-lg cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: mapAvatarPathActive[key] ? "#374151" : "#6B7280"
|
||||
}}
|
||||
>
|
||||
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
alt={key}
|
||||
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
|
||||
width={200}
|
||||
height={200} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start h-full">
|
||||
<ul className="grid grid-cols-3 sm:grid-cols-2 lg:grid-cols-3 gap-2 w-full h-[65vh] overflow-y-scroll overflow-x-hidden">
|
||||
{listAvatar.map((item, index) => (
|
||||
<div key={index} onClick={() => {
|
||||
setAvatarSelected(item);
|
||||
setSkillIDSelected(null)
|
||||
if (onClose) onClose()
|
||||
}}>
|
||||
<CharacterCard data={item} />
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+529
-529
File diff suppressed because it is too large
Load Diff
@@ -1,83 +1,83 @@
|
||||
"use client";
|
||||
import { getNameChar } from '@/helper';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import { AvatarDetail } from '@/types';
|
||||
import ParseText from '../parseText';
|
||||
import Image from 'next/image';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import useDetailDataStore from '@/stores/detailDataStore';
|
||||
|
||||
interface CharacterCardProps {
|
||||
data: AvatarDetail
|
||||
}
|
||||
|
||||
export default function CharacterCard({ data }: CharacterCardProps) {
|
||||
const { locale } = useLocaleStore();
|
||||
const transI18n = useTranslations("DataPage");
|
||||
const { baseType, damageType } = useDetailDataStore()
|
||||
|
||||
return (
|
||||
<li
|
||||
className="z-10 flex flex-col items-center rounded-xl shadow-xl
|
||||
bg-linear-to-br from-base-300 via-base-100 to-warning/70
|
||||
transform transition-transform duration-300 ease-in-out
|
||||
hover:scale-105 cursor-pointer min-h-45 sm:min-h-45 md:min-h-52.5 lg:min-h-55 xl:min-h-60 2xl:min-h-65"
|
||||
>
|
||||
<div
|
||||
className={`w-full rounded-md bg-linear-to-br ${data.Rarity === "CombatPowerAvatarRarityType5"
|
||||
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
|
||||
: "from-purple-400 via-purple-600/70 to-purple-800/50"
|
||||
}`}
|
||||
>
|
||||
|
||||
<div className="relative w-full h-32 lg:h-26 xl:h-36">
|
||||
<Image
|
||||
width={376}
|
||||
height={512}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${data.Image.AvatarIconPath}`}
|
||||
priority={true}
|
||||
className="rounded-md w-full h-full object-contain"
|
||||
alt="ALT"
|
||||
/>
|
||||
<Image
|
||||
width={32}
|
||||
height={32}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${damageType?.[data.DamageType].Icon}`}
|
||||
className="absolute top-0 left-0 w-6 h-6 rounded-full"
|
||||
alt={data.DamageType.toLowerCase()}
|
||||
/>
|
||||
<Image
|
||||
width={32}
|
||||
height={32}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${baseType?.[data.BaseType].Icon}`}
|
||||
className="absolute top-0 right-0 w-6 h-6 rounded-full"
|
||||
alt={data.BaseType.toLowerCase()}
|
||||
style={{
|
||||
boxShadow: "inset 0 0 8px 4px #9CA3AF"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ParseText
|
||||
locale={locale}
|
||||
text={getNameChar(locale, transI18n, data)}
|
||||
className="
|
||||
w-full px-0.5
|
||||
my-1
|
||||
text-center font-bold text-shadow-white
|
||||
leading-tight
|
||||
wrap-break-word
|
||||
text-sm sm:text-base 2xl:text-lg
|
||||
"
|
||||
/>
|
||||
</li>
|
||||
|
||||
);
|
||||
}
|
||||
"use client";
|
||||
import { getNameChar } from '@/helper';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import { AvatarDetail } from '@/types';
|
||||
import ParseText from '../parseText';
|
||||
import Image from 'next/image';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import useDetailDataStore from '@/stores/detailDataStore';
|
||||
|
||||
interface CharacterCardProps {
|
||||
data: AvatarDetail
|
||||
}
|
||||
|
||||
export default function CharacterCard({ data }: CharacterCardProps) {
|
||||
const { locale } = useLocaleStore();
|
||||
const transI18n = useTranslations("DataPage");
|
||||
const { baseType, damageType } = useDetailDataStore()
|
||||
|
||||
return (
|
||||
<li
|
||||
className="z-10 flex flex-col items-center rounded-xl shadow-xl
|
||||
bg-linear-to-br from-base-300 via-base-100 to-warning/70
|
||||
transform transition-transform duration-300 ease-in-out
|
||||
hover:scale-105 cursor-pointer min-h-45 sm:min-h-45 md:min-h-52.5 lg:min-h-55 xl:min-h-60 2xl:min-h-65"
|
||||
>
|
||||
<div
|
||||
className={`w-full rounded-md bg-linear-to-br ${data.Rarity === "CombatPowerAvatarRarityType5"
|
||||
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
|
||||
: "from-purple-400 via-purple-600/70 to-purple-800/50"
|
||||
}`}
|
||||
>
|
||||
|
||||
<div className="relative w-full h-32 lg:h-26 xl:h-36">
|
||||
<Image
|
||||
width={376}
|
||||
height={512}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${data.Image.AvatarIconPath}`}
|
||||
priority={true}
|
||||
className="rounded-md w-full h-full object-contain"
|
||||
alt="ALT"
|
||||
/>
|
||||
<Image
|
||||
width={32}
|
||||
height={32}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${damageType?.[data.DamageType].Icon}`}
|
||||
className="absolute top-0 left-0 w-6 h-6 rounded-full"
|
||||
alt={data.DamageType.toLowerCase()}
|
||||
/>
|
||||
<Image
|
||||
width={32}
|
||||
height={32}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${baseType?.[data.BaseType].Icon}`}
|
||||
className="absolute top-0 right-0 w-6 h-6 rounded-full"
|
||||
alt={data.BaseType.toLowerCase()}
|
||||
style={{
|
||||
boxShadow: "inset 0 0 8px 4px #9CA3AF"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ParseText
|
||||
locale={locale}
|
||||
text={getNameChar(locale, transI18n, data)}
|
||||
className="
|
||||
w-full px-0.5
|
||||
my-1
|
||||
text-center font-bold text-shadow-white
|
||||
leading-tight
|
||||
wrap-break-word
|
||||
text-sm sm:text-base 2xl:text-lg
|
||||
"
|
||||
/>
|
||||
</li>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,125 +1,125 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { CharacterInfoCardType } from '@/types';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import Image from 'next/image';
|
||||
import ParseText from '../parseText';
|
||||
import useDetailDataStore from '@/stores/detailDataStore';
|
||||
import { getLocaleName } from '@/helper/getName';
|
||||
|
||||
export default function CharacterInfoCard({ character, selectedCharacters, onCharacterToggle }: { character: CharacterInfoCardType, selectedCharacters: CharacterInfoCardType[], onCharacterToggle: (characterId: CharacterInfoCardType) => void }) {
|
||||
const isSelected = selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id);
|
||||
const { mapAvatar, mapLightCone, baseType, damageType } = useDetailDataStore();
|
||||
const { locale } = useLocaleStore();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-base-200/60 rounded-xl p-4 border cursor-pointer transition-all duration-200 ${isSelected
|
||||
? 'border-blue-400 ring-2 ring-blue-400/50'
|
||||
: 'border-base-300/50 hover:border-base-300 opacity-75'
|
||||
}`}
|
||||
|
||||
onClick={() => onCharacterToggle(character)}
|
||||
>
|
||||
|
||||
{/* Character Portrait */}
|
||||
<div className="relative mb-4">
|
||||
<div className="w-full h-48 rounded-lg overflow-hidden relative">
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/${mapAvatar?.[character.avatar_id.toString()]?.Image?.AvatarIconPath}`}
|
||||
alt={getLocaleName(locale, mapAvatar?.[character.avatar_id.toString()]?.Name)}
|
||||
width={376}
|
||||
height={512}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${damageType?.[mapAvatar?.[character.avatar_id.toString()]?.DamageType || ""]?.Icon}`}
|
||||
className="absolute top-0 left-0 w-10 h-10 rounded-full"
|
||||
alt={mapAvatar[character.avatar_id.toString()]?.DamageType.toLowerCase()}
|
||||
/>
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${baseType?.[mapAvatar?.[character.avatar_id.toString()]?.BaseType || ""]?.Icon}`}
|
||||
className="absolute top-0 right-0 w-10 h-10 rounded-full"
|
||||
alt={mapAvatar[character.avatar_id.toString()]?.BaseType.toLowerCase()}
|
||||
style={{
|
||||
boxShadow: "inset 0 0 8px 4px #9CA3AF"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Character Name and Level */}
|
||||
<div className="w-full rounded-lg flex items-center justify-center mb-2">
|
||||
<div className="text-center">
|
||||
<ParseText className="text-lg font-bold"
|
||||
text={getLocaleName(locale, mapAvatar[character.avatar_id.toString()]?.Name)}
|
||||
locale={locale}
|
||||
/>
|
||||
<div className="text-base mb-1">Lv.{character.level} E{character.rank}</div>
|
||||
</div>
|
||||
</div>
|
||||
{character.relics.length > 0 && (
|
||||
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
|
||||
{character.relics.map((relic, index) => (
|
||||
<div key={index} className="relative">
|
||||
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50">
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
|
||||
alt="Relic"
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
width={124}
|
||||
height={124}
|
||||
className="w-14 h-14 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white text-xs px-1 rounded">
|
||||
+{relic.level}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Light Cone */}
|
||||
{character.lightcone.item_id && (
|
||||
<div className="">
|
||||
<div className="rounded-lg h-42 flex items-center justify-center">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${mapLightCone?.[character.lightcone.item_id.toString()]?.Image?.ImagePath}`}
|
||||
alt={getLocaleName(locale, mapLightCone?.[character.lightcone.item_id.toString()]?.Name)}
|
||||
width={348}
|
||||
height={408}
|
||||
className="w-full h-full object-contain rounded-lg"
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className="w-full h-full rounded-lg flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold">
|
||||
<ParseText
|
||||
text={getLocaleName(locale, mapLightCone[character.lightcone.item_id.toString()]?.Name)}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-base mb-1">Lv.{character.lightcone.level} S{character.lightcone.rank}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { CharacterInfoCardType } from '@/types';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import Image from 'next/image';
|
||||
import ParseText from '../parseText';
|
||||
import useDetailDataStore from '@/stores/detailDataStore';
|
||||
import { getLocaleName } from '@/helper/getName';
|
||||
|
||||
export default function CharacterInfoCard({ character, selectedCharacters, onCharacterToggle }: { character: CharacterInfoCardType, selectedCharacters: CharacterInfoCardType[], onCharacterToggle: (characterId: CharacterInfoCardType) => void }) {
|
||||
const isSelected = selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id);
|
||||
const { mapAvatar, mapLightCone, baseType, damageType } = useDetailDataStore();
|
||||
const { locale } = useLocaleStore();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-base-200/60 rounded-xl p-4 border cursor-pointer transition-all duration-200 ${isSelected
|
||||
? 'border-blue-400 ring-2 ring-blue-400/50'
|
||||
: 'border-base-300/50 hover:border-base-300 opacity-75'
|
||||
}`}
|
||||
|
||||
onClick={() => onCharacterToggle(character)}
|
||||
>
|
||||
|
||||
{/* Character Portrait */}
|
||||
<div className="relative mb-4">
|
||||
<div className="w-full h-48 rounded-lg overflow-hidden relative">
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/${mapAvatar?.[character.avatar_id.toString()]?.Image?.AvatarIconPath}`}
|
||||
alt={getLocaleName(locale, mapAvatar?.[character.avatar_id.toString()]?.Name)}
|
||||
width={376}
|
||||
height={512}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${damageType?.[mapAvatar?.[character.avatar_id.toString()]?.DamageType || ""]?.Icon}`}
|
||||
className="absolute top-0 left-0 w-10 h-10 rounded-full"
|
||||
alt={mapAvatar[character.avatar_id.toString()]?.DamageType.toLowerCase()}
|
||||
/>
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${baseType?.[mapAvatar?.[character.avatar_id.toString()]?.BaseType || ""]?.Icon}`}
|
||||
className="absolute top-0 right-0 w-10 h-10 rounded-full"
|
||||
alt={mapAvatar[character.avatar_id.toString()]?.BaseType.toLowerCase()}
|
||||
style={{
|
||||
boxShadow: "inset 0 0 8px 4px #9CA3AF"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Character Name and Level */}
|
||||
<div className="w-full rounded-lg flex items-center justify-center mb-2">
|
||||
<div className="text-center">
|
||||
<ParseText className="text-lg font-bold"
|
||||
text={getLocaleName(locale, mapAvatar[character.avatar_id.toString()]?.Name)}
|
||||
locale={locale}
|
||||
/>
|
||||
<div className="text-base mb-1">Lv.{character.level} E{character.rank}</div>
|
||||
</div>
|
||||
</div>
|
||||
{character.relics.length > 0 && (
|
||||
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
|
||||
{character.relics.map((relic, index) => (
|
||||
<div key={index} className="relative">
|
||||
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50">
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
|
||||
alt="Relic"
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
width={124}
|
||||
height={124}
|
||||
className="w-14 h-14 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white text-xs px-1 rounded">
|
||||
+{relic.level}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Light Cone */}
|
||||
{character.lightcone.item_id && (
|
||||
<div className="">
|
||||
<div className="rounded-lg h-42 flex items-center justify-center">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${mapLightCone?.[character.lightcone.item_id.toString()]?.Image?.ImagePath}`}
|
||||
alt={getLocaleName(locale, mapLightCone?.[character.lightcone.item_id.toString()]?.Name)}
|
||||
width={348}
|
||||
height={408}
|
||||
className="w-full h-full object-contain rounded-lg"
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className="w-full h-full rounded-lg flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold">
|
||||
<ParseText
|
||||
text={getLocaleName(locale, mapLightCone[character.lightcone.item_id.toString()]?.Name)}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-base mb-1">Lv.{character.lightcone.level} S{character.lightcone.rank}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,53 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { getLocaleName } from '@/helper';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import ParseText from '../parseText';
|
||||
import Image from 'next/image';
|
||||
import { LightConeDetail } from '@/types';
|
||||
|
||||
interface LightconeCardProps {
|
||||
data: LightConeDetail
|
||||
}
|
||||
|
||||
export default function LightconeCard({ data }: LightconeCardProps) {
|
||||
|
||||
const { locale } = useLocaleStore();
|
||||
const text = getLocaleName(locale, data.Name)
|
||||
return (
|
||||
<li className="z-10 flex flex-col items-center rounded-md shadow-lg
|
||||
bg-linear-to-b from-customStart to-customEnd transform transition-transform duration-300
|
||||
hover:scale-105 cursor-pointer min-h-55"
|
||||
>
|
||||
<div
|
||||
className={`w-full rounded-md bg-linear-to-br ${data.Rarity === "CombatPowerLightconeRarity5"
|
||||
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
|
||||
: data.Rarity === "CombatPowerLightconeRarity4" ? "from-purple-400 via-purple-600/70 to-purple-800/50" :
|
||||
"from-blue-400 via-blue-600/70 to-blue-800/50"
|
||||
}`}
|
||||
>
|
||||
|
||||
<div className="relative w-full h-full">
|
||||
<Image
|
||||
loading="lazy"
|
||||
src={`${process.env.CDN_URL}/${data?.Image?.ThumbnailPath}`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
width={348}
|
||||
height={408}
|
||||
className="w-full h-full rounded-md object-cover"
|
||||
alt="ALT"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ParseText
|
||||
locale={locale}
|
||||
text={text}
|
||||
className="mt-2 px-1 text-center text-sm sm:text-base font-bold leading-tight"
|
||||
/>
|
||||
</li>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
"use client";
|
||||
|
||||
import { getLocaleName } from '@/helper';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import ParseText from '../parseText';
|
||||
import Image from 'next/image';
|
||||
import { LightConeDetail } from '@/types';
|
||||
|
||||
interface LightconeCardProps {
|
||||
data: LightConeDetail
|
||||
}
|
||||
|
||||
export default function LightconeCard({ data }: LightconeCardProps) {
|
||||
|
||||
const { locale } = useLocaleStore();
|
||||
const text = getLocaleName(locale, data.Name)
|
||||
return (
|
||||
<li className="z-10 flex flex-col items-center rounded-md shadow-lg
|
||||
bg-linear-to-b from-customStart to-customEnd transform transition-transform duration-300
|
||||
hover:scale-105 cursor-pointer min-h-55"
|
||||
>
|
||||
<div
|
||||
className={`w-full rounded-md bg-linear-to-br ${data.Rarity === "CombatPowerLightconeRarity5"
|
||||
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
|
||||
: data.Rarity === "CombatPowerLightconeRarity4" ? "from-purple-400 via-purple-600/70 to-purple-800/50" :
|
||||
"from-blue-400 via-blue-600/70 to-blue-800/50"
|
||||
}`}
|
||||
>
|
||||
|
||||
<div className="relative w-full h-full">
|
||||
<Image
|
||||
loading="lazy"
|
||||
src={`${process.env.CDN_URL}/${data?.Image?.ThumbnailPath}`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
width={348}
|
||||
height={408}
|
||||
className="w-full h-full rounded-md object-cover"
|
||||
alt="ALT"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ParseText
|
||||
locale={locale}
|
||||
text={text}
|
||||
className="mt-2 px-1 text-center text-sm sm:text-base font-bold leading-tight"
|
||||
/>
|
||||
</li>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,79 +1,79 @@
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { AvatarProfileCardType } from '@/types';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import Image from 'next/image';
|
||||
import ParseText from '../parseText';
|
||||
import useDetailDataStore from '@/stores/detailDataStore';
|
||||
import { getLocaleName } from '@/helper/getName';
|
||||
|
||||
|
||||
export default function ProfileCard({ profile, selectedProfile, onProfileToggle }: { profile: AvatarProfileCardType, selectedProfile: AvatarProfileCardType[], onProfileToggle: (profileId: AvatarProfileCardType) => void }) {
|
||||
const isSelected = selectedProfile.some((selectedProfile) => selectedProfile.key === profile.key);
|
||||
const { mapLightCone } = useDetailDataStore();
|
||||
const { locale } = useLocaleStore();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-base-200/60 rounded-xl p-4 border cursor-pointer transition-all duration-200 ${isSelected
|
||||
? 'border-blue-400 ring-2 ring-blue-400/50'
|
||||
: 'border-base-300/50 hover:border-base-300 opacity-75'
|
||||
}`}
|
||||
|
||||
onClick={() => onProfileToggle(profile)}
|
||||
>
|
||||
{/* Light Cone */}
|
||||
{profile.lightcone && (
|
||||
<div className="">
|
||||
<div className="rounded-lg h-42 flex items-center justify-center">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/spriteoutput/lightconemaxfigures/${profile.lightcone.item_id}.png`}
|
||||
alt={getLocaleName(locale, mapLightCone[profile.lightcone.item_id.toString()]?.Name)}
|
||||
width={348}
|
||||
height={408}
|
||||
className="w-full h-full object-contain rounded-lg"
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className="w-full h-full rounded-lg flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold">
|
||||
<ParseText
|
||||
text={getLocaleName(locale, mapLightCone[profile.lightcone.item_id.toString()]?.Name)}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-base mb-1">Lv.{profile.lightcone.level} S{profile.lightcone.rank}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.keys(profile.relics).length > 0 && (
|
||||
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
|
||||
{Object.values(profile.relics).map((relic, index) => (
|
||||
<div key={index} className="relative">
|
||||
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
|
||||
alt="Relic"
|
||||
width={124}
|
||||
height={124}
|
||||
className="w-14 h-14 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white text-xs px-1 rounded">
|
||||
+{relic.level}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { AvatarProfileCardType } from '@/types';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import Image from 'next/image';
|
||||
import ParseText from '../parseText';
|
||||
import useDetailDataStore from '@/stores/detailDataStore';
|
||||
import { getLocaleName } from '@/helper/getName';
|
||||
|
||||
|
||||
export default function ProfileCard({ profile, selectedProfile, onProfileToggle }: { profile: AvatarProfileCardType, selectedProfile: AvatarProfileCardType[], onProfileToggle: (profileId: AvatarProfileCardType) => void }) {
|
||||
const isSelected = selectedProfile.some((selectedProfile) => selectedProfile.key === profile.key);
|
||||
const { mapLightCone } = useDetailDataStore();
|
||||
const { locale } = useLocaleStore();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`bg-base-200/60 rounded-xl p-4 border cursor-pointer transition-all duration-200 ${isSelected
|
||||
? 'border-blue-400 ring-2 ring-blue-400/50'
|
||||
: 'border-base-300/50 hover:border-base-300 opacity-75'
|
||||
}`}
|
||||
|
||||
onClick={() => onProfileToggle(profile)}
|
||||
>
|
||||
{/* Light Cone */}
|
||||
{profile.lightcone && (
|
||||
<div className="">
|
||||
<div className="rounded-lg h-42 flex items-center justify-center">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/spriteoutput/lightconemaxfigures/${profile.lightcone.item_id}.png`}
|
||||
alt={getLocaleName(locale, mapLightCone[profile.lightcone.item_id.toString()]?.Name)}
|
||||
width={348}
|
||||
height={408}
|
||||
className="w-full h-full object-contain rounded-lg"
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className="w-full h-full rounded-lg flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="text-lg font-bold">
|
||||
<ParseText
|
||||
text={getLocaleName(locale, mapLightCone[profile.lightcone.item_id.toString()]?.Name)}
|
||||
locale={locale}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-base mb-1">Lv.{profile.lightcone.level} S{profile.lightcone.rank}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.keys(profile.relics).length > 0 && (
|
||||
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
|
||||
{Object.values(profile.relics).map((relic, index) => (
|
||||
<div key={index} className="relative">
|
||||
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
|
||||
alt="Relic"
|
||||
width={124}
|
||||
height={124}
|
||||
className="w-14 h-14 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white text-xs px-1 rounded">
|
||||
+{relic.level}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
+195
-195
@@ -1,196 +1,196 @@
|
||||
"use client";
|
||||
|
||||
import useRelicMakerStore from "@/stores/relicMakerStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import Image from "next/image";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface RelicCardProps {
|
||||
slot: string
|
||||
avatarId: string
|
||||
}
|
||||
const getRarityColor = (rarity: string) => {
|
||||
switch (rarity) {
|
||||
case '3': return 'border-green-500 shadow-green-500/50 bg-linear-to-br from-green-700 via-green-400 to-green-500';
|
||||
case '4': return 'border-blue-500 shadow-blue-500/50 bg-linear-to-br from-blue-700 via-blue-400 to-blue-500';
|
||||
case '5': return 'border-purple-500 shadow-purple-500/50 bg-linear-to-br from-purple-700 via-purple-400 to-purple-500';
|
||||
case '6': return 'border-yellow-500 shadow-yellow-500/50 bg-linear-to-br from-yellow-700 via-yellow-400 to-yellow-500';
|
||||
default: return 'border-gray-500 shadow-gray-500/50';
|
||||
}
|
||||
};
|
||||
|
||||
const getRarityName = (slot: string) => {
|
||||
switch (slot) {
|
||||
case '1': return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src="/relics/HEAD.png"
|
||||
alt="Head"
|
||||
width={20}
|
||||
height={20}
|
||||
className="bg-black/50 rounded-full"
|
||||
/>
|
||||
<h2>Head</h2>
|
||||
</div>
|
||||
);
|
||||
case '2': return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src="/relics/HAND.png"
|
||||
alt="Hand"
|
||||
width={20}
|
||||
height={20}
|
||||
className="bg-black/50 rounded-full"
|
||||
/>
|
||||
<h2>Hands</h2>
|
||||
</div>
|
||||
);
|
||||
case '3': return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src="/relics/BODY.png"
|
||||
alt="Body"
|
||||
width={20}
|
||||
height={20}
|
||||
className="bg-black/50 rounded-full"
|
||||
/>
|
||||
<h2>Body</h2>
|
||||
</div>
|
||||
);
|
||||
case '4': return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src="/relics/FOOT.png"
|
||||
alt="Foot"
|
||||
width={20}
|
||||
height={20}
|
||||
className="bg-black/50 rounded-full"
|
||||
/>
|
||||
<h2>Feet</h2>
|
||||
</div>
|
||||
);
|
||||
case '5': return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src="/relics/NECK.png"
|
||||
alt="Neck"
|
||||
width={20}
|
||||
height={20}
|
||||
className="bg-black/50 rounded-full"
|
||||
/>
|
||||
<h2>Planar sphere</h2>
|
||||
</div>
|
||||
);
|
||||
case '6': return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src="/relics/OBJECT.png"
|
||||
alt="Object"
|
||||
width={20}
|
||||
height={20}
|
||||
className="bg-black/50 rounded-full"
|
||||
/>
|
||||
<h2>Link rope</h2>
|
||||
</div>
|
||||
);
|
||||
default: return '';
|
||||
}
|
||||
};
|
||||
export default function RelicCard({ slot, avatarId }: RelicCardProps) {
|
||||
const { avatars } = useUserDataStore()
|
||||
const { selectedRelicSlot } = useRelicMakerStore()
|
||||
|
||||
const relicDetail = useMemo(() => {
|
||||
const avatar = avatars[avatarId];
|
||||
if (avatar) {
|
||||
if (avatar.profileList[avatar.profileSelect].relics[slot]) {
|
||||
return avatar.profileList[avatar.profileSelect].relics[slot];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}, [avatars, avatarId, slot]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{relicDetail ? (
|
||||
<div
|
||||
className="flex flex-col items-center cursor-pointer ">
|
||||
<div
|
||||
className={`
|
||||
relative w-24 h-24 rounded-full
|
||||
${getRarityColor(relicDetail.relic_id.toString()[0])}
|
||||
shadow-xl
|
||||
flex items-center justify-center
|
||||
cursor-pointer transition-transform
|
||||
${selectedRelicSlot === slot ? 'ring-5 ring-success scale-105' : 'ring-3 ring-primary'}
|
||||
`}
|
||||
>
|
||||
<span>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relicDetail.relic_set_id}_${slot}.png`}
|
||||
alt="Relic"
|
||||
width={124}
|
||||
height={124}
|
||||
className="w-14 h-14 object-contain"
|
||||
/>
|
||||
</span>
|
||||
|
||||
{/* Level Badge */}
|
||||
<div className="absolute -bottom-2 bg-base-100 border-2 border-base-300 rounded-full px-2 py-1">
|
||||
<span className="text-sm font-bold text-primary">+{relicDetail.level}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-center">
|
||||
<div className="text-sm font-medium text-base-content">{getRarityName(slot)}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex flex-col items-center cursor-pointer">
|
||||
<div
|
||||
className={`
|
||||
relative w-24 h-24 rounded-full border-4
|
||||
${getRarityColor("None")}
|
||||
bg-base-300 shadow-xl
|
||||
flex items-center justify-center
|
||||
cursor-pointer hover:scale-105 transition-transform
|
||||
ring-4 ring-primary
|
||||
`}
|
||||
>
|
||||
<span className="text-3xl">
|
||||
<svg className="w-12 h-12 text-base-content/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
{/* Level Badge */}
|
||||
<div className="absolute -bottom-2 bg-base-100 border-2 border-base-300 rounded-full px-2 py-1">
|
||||
<span className="text-sm font-bold text-primary">+{0}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-center">
|
||||
<div>{getRarityName(slot)}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
"use client";
|
||||
|
||||
import useRelicMakerStore from "@/stores/relicMakerStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import Image from "next/image";
|
||||
import { useMemo } from "react";
|
||||
|
||||
interface RelicCardProps {
|
||||
slot: string
|
||||
avatarId: string
|
||||
}
|
||||
const getRarityColor = (rarity: string) => {
|
||||
switch (rarity) {
|
||||
case '3': return 'border-green-500 shadow-green-500/50 bg-linear-to-br from-green-700 via-green-400 to-green-500';
|
||||
case '4': return 'border-blue-500 shadow-blue-500/50 bg-linear-to-br from-blue-700 via-blue-400 to-blue-500';
|
||||
case '5': return 'border-purple-500 shadow-purple-500/50 bg-linear-to-br from-purple-700 via-purple-400 to-purple-500';
|
||||
case '6': return 'border-yellow-500 shadow-yellow-500/50 bg-linear-to-br from-yellow-700 via-yellow-400 to-yellow-500';
|
||||
default: return 'border-gray-500 shadow-gray-500/50';
|
||||
}
|
||||
};
|
||||
|
||||
const getRarityName = (slot: string) => {
|
||||
switch (slot) {
|
||||
case '1': return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src="/relics/HEAD.png"
|
||||
alt="Head"
|
||||
width={20}
|
||||
height={20}
|
||||
className="bg-black/50 rounded-full"
|
||||
/>
|
||||
<h2>Head</h2>
|
||||
</div>
|
||||
);
|
||||
case '2': return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src="/relics/HAND.png"
|
||||
alt="Hand"
|
||||
width={20}
|
||||
height={20}
|
||||
className="bg-black/50 rounded-full"
|
||||
/>
|
||||
<h2>Hands</h2>
|
||||
</div>
|
||||
);
|
||||
case '3': return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src="/relics/BODY.png"
|
||||
alt="Body"
|
||||
width={20}
|
||||
height={20}
|
||||
className="bg-black/50 rounded-full"
|
||||
/>
|
||||
<h2>Body</h2>
|
||||
</div>
|
||||
);
|
||||
case '4': return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src="/relics/FOOT.png"
|
||||
alt="Foot"
|
||||
width={20}
|
||||
height={20}
|
||||
className="bg-black/50 rounded-full"
|
||||
/>
|
||||
<h2>Feet</h2>
|
||||
</div>
|
||||
);
|
||||
case '5': return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src="/relics/NECK.png"
|
||||
alt="Neck"
|
||||
width={20}
|
||||
height={20}
|
||||
className="bg-black/50 rounded-full"
|
||||
/>
|
||||
<h2>Planar sphere</h2>
|
||||
</div>
|
||||
);
|
||||
case '6': return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src="/relics/OBJECT.png"
|
||||
alt="Object"
|
||||
width={20}
|
||||
height={20}
|
||||
className="bg-black/50 rounded-full"
|
||||
/>
|
||||
<h2>Link rope</h2>
|
||||
</div>
|
||||
);
|
||||
default: return '';
|
||||
}
|
||||
};
|
||||
export default function RelicCard({ slot, avatarId }: RelicCardProps) {
|
||||
const { avatars } = useUserDataStore()
|
||||
const { selectedRelicSlot } = useRelicMakerStore()
|
||||
|
||||
const relicDetail = useMemo(() => {
|
||||
const avatar = avatars[avatarId];
|
||||
if (avatar) {
|
||||
if (avatar.profileList[avatar.profileSelect].relics[slot]) {
|
||||
return avatar.profileList[avatar.profileSelect].relics[slot];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}, [avatars, avatarId, slot]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{relicDetail ? (
|
||||
<div
|
||||
className="flex flex-col items-center cursor-pointer ">
|
||||
<div
|
||||
className={`
|
||||
relative w-24 h-24 rounded-full
|
||||
${getRarityColor(relicDetail.relic_id.toString()[0])}
|
||||
shadow-xl
|
||||
flex items-center justify-center
|
||||
cursor-pointer transition-transform
|
||||
${selectedRelicSlot === slot ? 'ring-5 ring-success scale-105' : 'ring-3 ring-primary'}
|
||||
`}
|
||||
>
|
||||
<span>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relicDetail.relic_set_id}_${slot}.png`}
|
||||
alt="Relic"
|
||||
width={124}
|
||||
height={124}
|
||||
className="w-14 h-14 object-contain"
|
||||
/>
|
||||
</span>
|
||||
|
||||
{/* Level Badge */}
|
||||
<div className="absolute -bottom-2 bg-base-100 border-2 border-base-300 rounded-full px-2 py-1">
|
||||
<span className="text-sm font-bold text-primary">+{relicDetail.level}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-center">
|
||||
<div className="text-sm font-medium text-base-content">{getRarityName(slot)}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex flex-col items-center cursor-pointer">
|
||||
<div
|
||||
className={`
|
||||
relative w-24 h-24 rounded-full border-4
|
||||
${getRarityColor("None")}
|
||||
bg-base-300 shadow-xl
|
||||
flex items-center justify-center
|
||||
cursor-pointer hover:scale-105 transition-transform
|
||||
ring-4 ring-primary
|
||||
`}
|
||||
>
|
||||
<span className="text-3xl">
|
||||
<svg className="w-12 h-12 text-base-content/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
{/* Level Badge */}
|
||||
<div className="absolute -bottom-2 bg-base-100 border-2 border-base-300 rounded-full px-2 py-1">
|
||||
<span className="text-sm font-bold text-primary">+{0}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 text-center">
|
||||
<div>{getRarityName(slot)}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,69 +1,69 @@
|
||||
"use client";
|
||||
import Image from 'next/image';
|
||||
import { AvatarDetail } from '@/types';
|
||||
import useDetailDataStore from '@/stores/detailDataStore';
|
||||
|
||||
|
||||
interface SimpleAvatarCardProps {
|
||||
data: AvatarDetail;
|
||||
isSelected?: boolean;
|
||||
onClick?: () => void;
|
||||
showRemoveHover?: boolean;
|
||||
}
|
||||
|
||||
export const SimpleAvatarCard = ({ data, isSelected, onClick, showRemoveHover }: SimpleAvatarCardProps) => {
|
||||
const { baseType, damageType } = useDetailDataStore()
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={`relative w-16 h-16 sm:w-20 sm:h-20 rounded-md cursor-pointer transition-transform duration-200 ease-in-out hover:scale-105 shadow-md shrink-0
|
||||
${isSelected ? 'ring-2 ring-success opacity-60' : ''}
|
||||
bg-linear-to-br ${data.Rarity === "CombatPowerAvatarRarityType5"
|
||||
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
|
||||
: "from-purple-400 via-purple-600/70 to-purple-800/50"
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
width={80}
|
||||
height={80}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${data.Image.ActionAvatarHeadIconPath}`}
|
||||
priority={true}
|
||||
className="w-full h-full object-contain"
|
||||
alt="Avatar"
|
||||
/>
|
||||
<div className="absolute top-0 left-0 w-5 h-5 bg-black/40 rounded-full flex items-center justify-center p-0.5">
|
||||
<Image
|
||||
width={20}
|
||||
height={20}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${damageType?.[data.DamageType]?.Icon}`}
|
||||
className="w-full h-full object-contain"
|
||||
alt="Element"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute top-0 right-0 w-5 h-5 bg-black/40 rounded-full flex items-center justify-center p-0.5">
|
||||
<Image
|
||||
width={20}
|
||||
height={20}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${baseType?.[data.BaseType]?.Icon}`}
|
||||
className="w-full h-full object-contain"
|
||||
alt="Path"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showRemoveHover && (
|
||||
<div className="absolute inset-0 bg-error/80 rounded-md flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
"use client";
|
||||
import Image from 'next/image';
|
||||
import { AvatarDetail } from '@/types';
|
||||
import useDetailDataStore from '@/stores/detailDataStore';
|
||||
|
||||
|
||||
interface SimpleAvatarCardProps {
|
||||
data: AvatarDetail;
|
||||
isSelected?: boolean;
|
||||
onClick?: () => void;
|
||||
showRemoveHover?: boolean;
|
||||
}
|
||||
|
||||
export const SimpleAvatarCard = ({ data, isSelected, onClick, showRemoveHover }: SimpleAvatarCardProps) => {
|
||||
const { baseType, damageType } = useDetailDataStore()
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={`relative w-16 h-16 sm:w-20 sm:h-20 rounded-md cursor-pointer transition-transform duration-200 ease-in-out hover:scale-105 shadow-md shrink-0
|
||||
${isSelected ? 'ring-2 ring-success opacity-60' : ''}
|
||||
bg-linear-to-br ${data.Rarity === "CombatPowerAvatarRarityType5"
|
||||
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
|
||||
: "from-purple-400 via-purple-600/70 to-purple-800/50"
|
||||
}`}
|
||||
>
|
||||
<Image
|
||||
width={80}
|
||||
height={80}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${data.Image.ActionAvatarHeadIconPath}`}
|
||||
priority={true}
|
||||
className="w-full h-full object-contain"
|
||||
alt="Avatar"
|
||||
/>
|
||||
<div className="absolute top-0 left-0 w-5 h-5 bg-black/40 rounded-full flex items-center justify-center p-0.5">
|
||||
<Image
|
||||
width={20}
|
||||
height={20}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${damageType?.[data.DamageType]?.Icon}`}
|
||||
className="w-full h-full object-contain"
|
||||
alt="Element"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute top-0 right-0 w-5 h-5 bg-black/40 rounded-full flex items-center justify-center p-0.5">
|
||||
<Image
|
||||
width={20}
|
||||
height={20}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${baseType?.[data.BaseType]?.Icon}`}
|
||||
className="w-full h-full object-contain"
|
||||
alt="Path"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showRemoveHover && (
|
||||
<div className="absolute inset-0 bg-error/80 rounded-md flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,91 +1,91 @@
|
||||
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import { Check, Sparkles, Bug, Zap, Package, Calendar } from 'lucide-react';
|
||||
|
||||
export default function ChangelogBar() {
|
||||
const { changelog } = useLocaleStore()
|
||||
|
||||
const getIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'feature':
|
||||
return <Sparkles className="w-3 h-3 md:w-4 md:h-4" />;
|
||||
case 'fix':
|
||||
return <Bug className="w-3 h-3 md:w-4 md:h-4" />;
|
||||
case 'improvement':
|
||||
return <Zap className="w-3 h-3 md:w-4 md:h-4" />;
|
||||
default:
|
||||
return <Package className="w-3 h-3 md:w-4 md:h-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getBadgeClass = (type: string) => {
|
||||
switch (type) {
|
||||
case 'feature':
|
||||
return 'badge-success';
|
||||
case 'fix':
|
||||
return 'badge-error';
|
||||
case 'improvement':
|
||||
return 'badge-warning';
|
||||
default:
|
||||
return 'badge-info';
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case 'feature':
|
||||
return 'Feature';
|
||||
case 'fix':
|
||||
return 'Fix';
|
||||
case 'improvement':
|
||||
return 'Improvement';
|
||||
default:
|
||||
return 'Update';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="md:p-4">
|
||||
{/* Alert */}
|
||||
{/* <div className="alert alert-info mb-4 md:p-2 p-1">
|
||||
<AlertCircle className="w-6 h-6" />
|
||||
</div> */}
|
||||
|
||||
{/* Timeline */}
|
||||
<div className="flex flex-col gap-4">
|
||||
{changelog.map((change, index) => (
|
||||
<div key={index} className="bg-base-100 shadow-sm">
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Version Header */}
|
||||
<div className="flex flex-row items-center gap-2 md:gap-3">
|
||||
<div className={`badge ${getBadgeClass(change.type)} gap-1 md:gap-2 p-1`}>
|
||||
{getIcon(change.type)}
|
||||
{getTypeLabel(change.type)}
|
||||
</div>
|
||||
<h2 className="card-title text-sm md:text-lg">
|
||||
Version {change.version}
|
||||
</h2>
|
||||
<div className="flex items-center gap-1 md:gap-2 text-base-content/60 ml-auto">
|
||||
<Calendar className="w-3 h-3 md:w-4 md:h-4" />
|
||||
<span className="text-sm">{change.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Changes List */}
|
||||
<ul className="space-y-3">
|
||||
{change.items.map((item, itemIndex) => (
|
||||
<li key={itemIndex} className="flex items-start gap-3">
|
||||
<div className="mt-1">
|
||||
<Check className="w-5 h-5 text-success" />
|
||||
</div>
|
||||
<span className="text-base-content">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import { Check, Sparkles, Bug, Zap, Package, Calendar } from 'lucide-react';
|
||||
|
||||
export default function ChangelogBar() {
|
||||
const { changelog } = useLocaleStore()
|
||||
|
||||
const getIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'feature':
|
||||
return <Sparkles className="w-3 h-3 md:w-4 md:h-4" />;
|
||||
case 'fix':
|
||||
return <Bug className="w-3 h-3 md:w-4 md:h-4" />;
|
||||
case 'improvement':
|
||||
return <Zap className="w-3 h-3 md:w-4 md:h-4" />;
|
||||
default:
|
||||
return <Package className="w-3 h-3 md:w-4 md:h-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getBadgeClass = (type: string) => {
|
||||
switch (type) {
|
||||
case 'feature':
|
||||
return 'badge-success';
|
||||
case 'fix':
|
||||
return 'badge-error';
|
||||
case 'improvement':
|
||||
return 'badge-warning';
|
||||
default:
|
||||
return 'badge-info';
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeLabel = (type: string) => {
|
||||
switch (type) {
|
||||
case 'feature':
|
||||
return 'Feature';
|
||||
case 'fix':
|
||||
return 'Fix';
|
||||
case 'improvement':
|
||||
return 'Improvement';
|
||||
default:
|
||||
return 'Update';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="md:p-4">
|
||||
{/* Alert */}
|
||||
{/* <div className="alert alert-info mb-4 md:p-2 p-1">
|
||||
<AlertCircle className="w-6 h-6" />
|
||||
</div> */}
|
||||
|
||||
{/* Timeline */}
|
||||
<div className="flex flex-col gap-4">
|
||||
{changelog.map((change, index) => (
|
||||
<div key={index} className="bg-base-100 shadow-sm">
|
||||
<div className="flex flex-col gap-2">
|
||||
{/* Version Header */}
|
||||
<div className="flex flex-row items-center gap-2 md:gap-3">
|
||||
<div className={`badge ${getBadgeClass(change.type)} gap-1 md:gap-2 p-1`}>
|
||||
{getIcon(change.type)}
|
||||
{getTypeLabel(change.type)}
|
||||
</div>
|
||||
<h2 className="card-title text-sm md:text-lg">
|
||||
Version {change.version}
|
||||
</h2>
|
||||
<div className="flex items-center gap-1 md:gap-2 text-base-content/60 ml-auto">
|
||||
<Calendar className="w-3 h-3 md:w-4 md:h-4" />
|
||||
<span className="text-sm">{change.date}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Changes List */}
|
||||
<ul className="space-y-3">
|
||||
{change.items.map((item, itemIndex) => (
|
||||
<li key={itemIndex} className="flex items-start gap-3">
|
||||
<div className="mt-1">
|
||||
<Check className="w-5 h-5 text-success" />
|
||||
</div>
|
||||
<span className="text-base-content">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,56 +1,56 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
useFetchASGroupData,
|
||||
useFetchAvatarData,
|
||||
useFetchChangelog,
|
||||
useFetchConfigData,
|
||||
useFetchLightconeData,
|
||||
useFetchMOCGroupData,
|
||||
useFetchMonsterData,
|
||||
useFetchPeakGroupData,
|
||||
useFetchPFGroupData,
|
||||
useFetchRelicSetData
|
||||
} from "@/lib/hooks"
|
||||
|
||||
export default function ClientDataFetcher({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const q1 = useFetchConfigData()
|
||||
const q2 = useFetchAvatarData()
|
||||
const q3 = useFetchLightconeData()
|
||||
const q4 = useFetchRelicSetData()
|
||||
const q5 = useFetchMonsterData()
|
||||
const q6 = useFetchPFGroupData()
|
||||
const q7 = useFetchMOCGroupData()
|
||||
const q8 = useFetchASGroupData()
|
||||
const q9 = useFetchPeakGroupData()
|
||||
const q10 = useFetchChangelog()
|
||||
|
||||
const queries = [q1,q2,q3,q4,q5,q6,q7,q8,q9,q10]
|
||||
|
||||
const loading = queries.some(q => q.isLoading)
|
||||
|
||||
const progress =
|
||||
(queries.filter(q => q.isSuccess).length / queries.length) * 100
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-screen flex-col items-center justify-center gap-4">
|
||||
<div className="text-lg font-semibold">Loading data...</div>
|
||||
|
||||
<progress
|
||||
className="progress progress-primary w-56"
|
||||
value={progress}
|
||||
max="100"
|
||||
/>
|
||||
|
||||
<div>{Math.floor(progress)}%</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
"use client"
|
||||
|
||||
import {
|
||||
useFetchASGroupData,
|
||||
useFetchAvatarData,
|
||||
useFetchChangelog,
|
||||
useFetchConfigData,
|
||||
useFetchLightconeData,
|
||||
useFetchMOCGroupData,
|
||||
useFetchMonsterData,
|
||||
useFetchPeakGroupData,
|
||||
useFetchPFGroupData,
|
||||
useFetchRelicSetData
|
||||
} from "@/lib/hooks"
|
||||
|
||||
export default function ClientDataFetcher({
|
||||
children
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const q1 = useFetchConfigData()
|
||||
const q2 = useFetchAvatarData()
|
||||
const q3 = useFetchLightconeData()
|
||||
const q4 = useFetchRelicSetData()
|
||||
const q5 = useFetchMonsterData()
|
||||
const q6 = useFetchPFGroupData()
|
||||
const q7 = useFetchMOCGroupData()
|
||||
const q8 = useFetchASGroupData()
|
||||
const q9 = useFetchPeakGroupData()
|
||||
const q10 = useFetchChangelog()
|
||||
|
||||
const queries = [q1,q2,q3,q4,q5,q6,q7,q8,q9,q10]
|
||||
|
||||
const loading = queries.some(q => q.isLoading)
|
||||
|
||||
const progress =
|
||||
(queries.filter(q => q.isSuccess).length / queries.length) * 100
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-screen flex-col items-center justify-center gap-4">
|
||||
<div className="text-lg font-semibold">Loading data...</div>
|
||||
|
||||
<progress
|
||||
className="progress progress-primary w-56"
|
||||
value={progress}
|
||||
max="100"
|
||||
/>
|
||||
|
||||
<div>{Math.floor(progress)}%</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
+168
-168
@@ -1,169 +1,169 @@
|
||||
"use client"
|
||||
|
||||
import { connectToPS, syncDataToPS } from "@/helper"
|
||||
import useConnectStore from "@/stores/connectStore"
|
||||
import useGlobalStore from "@/stores/globalStore"
|
||||
import { PSConnectType } from "@/types"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { useState } from "react"
|
||||
|
||||
export default function ConnectBar() {
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const [message, setMessage] = useState({ text: '', type: '' });
|
||||
const {
|
||||
connectionType,
|
||||
privateType,
|
||||
serverUrl,
|
||||
username,
|
||||
password,
|
||||
setConnectionType,
|
||||
setPrivateType,
|
||||
setServerUrl,
|
||||
setUsername,
|
||||
setPassword
|
||||
} = useConnectStore()
|
||||
const { isConnectPS, setIsConnectPS } = useGlobalStore()
|
||||
|
||||
return (
|
||||
<div className="px-6 py-4">
|
||||
<div className="form-control grid grid-cols-1 w-full mb-6">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-purple-300">{transI18n("connectionType")}</span>
|
||||
</label>
|
||||
<select
|
||||
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
|
||||
value={connectionType}
|
||||
onChange={(e) => {
|
||||
setIsConnectPS(false)
|
||||
setConnectionType(e.target.value)
|
||||
}}
|
||||
>
|
||||
<option value={PSConnectType.FireflyGo}>FireflyGo</option>
|
||||
<option value={PSConnectType.RobinSR}>RobinSR</option>
|
||||
<option value={PSConnectType.Other}>{transI18n("other")}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{connectionType === PSConnectType.Other && (
|
||||
<div className="flex flex-col md:space-x-4 mb-6 gap-2">
|
||||
<div className="form-control w-full mb-4 md:mb-0">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-purple-300">{transI18n("serverUrl")}</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={transI18n("placeholderServerUrl")}
|
||||
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
|
||||
value={serverUrl}
|
||||
onChange={(e) => setServerUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-control w-full mb-4 md:mb-0">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-purple-300">{transI18n("privateType")}</span>
|
||||
</label>
|
||||
<select
|
||||
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
|
||||
value={privateType}
|
||||
onChange={(e) => setPrivateType(e.target.value)}
|
||||
>
|
||||
<option value="Local">{transI18n("local")}</option>
|
||||
<option value="Server">{transI18n("server")}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-control w-full mb-4 md:mb-0">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-purple-300">{transI18n("username")}</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={transI18n("placeholderUsername")}
|
||||
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-control w-full mb-4 md:mb-0">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-purple-300">{transI18n("password")}</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder={transI18n("placeholderPassword")}
|
||||
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message.text && (
|
||||
<div className={`alert ${message.type === 'success' ? 'alert-success' :
|
||||
message.type === 'error' ? 'alert-error' : 'alert-info'
|
||||
} mb-6`}>
|
||||
<span>{message.text}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 mb-2">
|
||||
<div className="flex items-center justify-center md:justify-start">
|
||||
<span className="text-md mr-2">{transI18n("status")}:</span>
|
||||
<span
|
||||
className={`badge ${isConnectPS ? "badge-success" : "badge-error"
|
||||
} badge-lg`}
|
||||
>
|
||||
{isConnectPS ? transI18n("connected") : transI18n("unconnected")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-2 w-full justify-center md:justify-end">
|
||||
<button
|
||||
onClick={async () => {
|
||||
const response = await connectToPS();
|
||||
if (response.success) {
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("connectedSuccess"),
|
||||
});
|
||||
} else {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: response.message,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="btn btn-primary w-full sm:w-auto"
|
||||
>
|
||||
{transI18n("connectPs")}
|
||||
</button>
|
||||
|
||||
{isConnectPS && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
const response = await syncDataToPS();
|
||||
if (response.success) {
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("syncSuccess"),
|
||||
});
|
||||
} else {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: `${transI18n("syncFailed")}: ${response.message}`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="btn btn-success w-full sm:w-auto"
|
||||
>
|
||||
{transI18n("sync")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
"use client"
|
||||
|
||||
import { connectToPS, syncDataToPS } from "@/helper"
|
||||
import useConnectStore from "@/stores/connectStore"
|
||||
import useGlobalStore from "@/stores/globalStore"
|
||||
import { PSConnectType } from "@/types"
|
||||
import { useTranslations } from "next-intl"
|
||||
import { useState } from "react"
|
||||
|
||||
export default function ConnectBar() {
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const [message, setMessage] = useState({ text: '', type: '' });
|
||||
const {
|
||||
connectionType,
|
||||
privateType,
|
||||
serverUrl,
|
||||
username,
|
||||
password,
|
||||
setConnectionType,
|
||||
setPrivateType,
|
||||
setServerUrl,
|
||||
setUsername,
|
||||
setPassword
|
||||
} = useConnectStore()
|
||||
const { isConnectPS, setIsConnectPS } = useGlobalStore()
|
||||
|
||||
return (
|
||||
<div className="px-6 py-4">
|
||||
<div className="form-control grid grid-cols-1 w-full mb-6">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-purple-300">{transI18n("connectionType")}</span>
|
||||
</label>
|
||||
<select
|
||||
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
|
||||
value={connectionType}
|
||||
onChange={(e) => {
|
||||
setIsConnectPS(false)
|
||||
setConnectionType(e.target.value)
|
||||
}}
|
||||
>
|
||||
<option value={PSConnectType.FireflyGo}>FireflyGo</option>
|
||||
<option value={PSConnectType.RobinSR}>RobinSR</option>
|
||||
<option value={PSConnectType.Other}>{transI18n("other")}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{connectionType === PSConnectType.Other && (
|
||||
<div className="flex flex-col md:space-x-4 mb-6 gap-2">
|
||||
<div className="form-control w-full mb-4 md:mb-0">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-purple-300">{transI18n("serverUrl")}</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={transI18n("placeholderServerUrl")}
|
||||
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
|
||||
value={serverUrl}
|
||||
onChange={(e) => setServerUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-control w-full mb-4 md:mb-0">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-purple-300">{transI18n("privateType")}</span>
|
||||
</label>
|
||||
<select
|
||||
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
|
||||
value={privateType}
|
||||
onChange={(e) => setPrivateType(e.target.value)}
|
||||
>
|
||||
<option value="Local">{transI18n("local")}</option>
|
||||
<option value="Server">{transI18n("server")}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="form-control w-full mb-4 md:mb-0">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-purple-300">{transI18n("username")}</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={transI18n("placeholderUsername")}
|
||||
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-control w-full mb-4 md:mb-0">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-purple-300">{transI18n("password")}</span>
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder={transI18n("placeholderPassword")}
|
||||
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message.text && (
|
||||
<div className={`alert ${message.type === 'success' ? 'alert-success' :
|
||||
message.type === 'error' ? 'alert-error' : 'alert-info'
|
||||
} mb-6`}>
|
||||
<span>{message.text}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 mb-2">
|
||||
<div className="flex items-center justify-center md:justify-start">
|
||||
<span className="text-md mr-2">{transI18n("status")}:</span>
|
||||
<span
|
||||
className={`badge ${isConnectPS ? "badge-success" : "badge-error"
|
||||
} badge-lg`}
|
||||
>
|
||||
{isConnectPS ? transI18n("connected") : transI18n("unconnected")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex flex-col sm:flex-row gap-2 w-full justify-center md:justify-end">
|
||||
<button
|
||||
onClick={async () => {
|
||||
const response = await connectToPS();
|
||||
if (response.success) {
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("connectedSuccess"),
|
||||
});
|
||||
} else {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: response.message,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="btn btn-primary w-full sm:w-auto"
|
||||
>
|
||||
{transI18n("connectPs")}
|
||||
</button>
|
||||
|
||||
{isConnectPS && (
|
||||
<button
|
||||
onClick={async () => {
|
||||
const response = await syncDataToPS();
|
||||
if (response.success) {
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("syncSuccess"),
|
||||
});
|
||||
} else {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: `${transI18n("syncFailed")}: ${response.message}`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="btn btn-success w-full sm:w-auto"
|
||||
>
|
||||
{transI18n("sync")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,85 +1,85 @@
|
||||
"use client"
|
||||
import { replaceByParam, getLocaleName } from '@/helper';
|
||||
import Image from "next/image";
|
||||
import ParseText from "../parseText";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||
import ExtraEffectList from '../extraInfo';
|
||||
|
||||
|
||||
export default function EidolonsInfo() {
|
||||
const { avatarSelected } = useCurrentDataStore()
|
||||
const { locale } = useLocaleStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { setAvatars, avatars } = useUserDataStore()
|
||||
|
||||
const charRank = useMemo(() => {
|
||||
if (!avatarSelected) return null;
|
||||
const avatar = avatars[avatarSelected.ID];
|
||||
if (avatar?.enhanced != "") {
|
||||
return avatarSelected?.Enhanced?.[avatar?.enhanced]?.Ranks
|
||||
}
|
||||
return avatarSelected?.Ranks
|
||||
}, [avatarSelected, avatars]);
|
||||
|
||||
return (
|
||||
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
|
||||
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
|
||||
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
|
||||
{transI18n("eidolons")}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 m-4 p-4 font-bold gap-4 w-fit max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
|
||||
{charRank && avatars[avatarSelected?.ID || ""] && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Object.entries(charRank || {}).map(([key, rank]) => (
|
||||
<div key={key}
|
||||
className="flex flex-col items-center"
|
||||
|
||||
>
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
let newRank = Number(rank.Rank)
|
||||
if (avatars[avatarSelected?.ID || ""]?.data?.rank == Number(rank.Rank)) {
|
||||
newRank = Number(rank.Rank) - 1
|
||||
}
|
||||
setAvatars({ ...avatars, [avatarSelected?.ID || ""]: { ...avatars[avatarSelected?.ID || ""], data: { ...avatars[avatarSelected?.ID || ""].data, rank: newRank } } })
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className={`w-60 object-contain mb-2 ${Number(rank.Rank) <= avatars[avatarSelected?.ID.toString() || ""]?.data?.rank ? "" : "grayscale"}`}
|
||||
src={`${process.env.CDN_URL}/ui/ui3d/rank/_dependencies/textures/${avatarSelected?.ID}/${avatarSelected?.ID}_Rank_${rank.Rank}.png`}
|
||||
alt={`Rank ${rank.Rank} Image`}
|
||||
priority
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
width={240}
|
||||
height={240}
|
||||
/>
|
||||
|
||||
<div className="text-lg pb-1 flex items-center justify-items-center gap-2">
|
||||
<span className="inline-block text-indigo-500">{rank.Rank}.</span>
|
||||
<ParseText
|
||||
locale={locale}
|
||||
text={getLocaleName(locale, rank.Name)}
|
||||
className="text-center text-base font-normal leading-tight"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm font-normal">
|
||||
<div dangerouslySetInnerHTML={{ __html: replaceByParam(getLocaleName(locale, rank.Desc), rank.Param) }} />
|
||||
<ExtraEffectList extras={rank?.Extra} locale={locale} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
"use client"
|
||||
import { replaceByParam, getLocaleName } from '@/helper';
|
||||
import Image from "next/image";
|
||||
import ParseText from "../parseText";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||
import ExtraEffectList from '../extraInfo';
|
||||
|
||||
|
||||
export default function EidolonsInfo() {
|
||||
const { avatarSelected } = useCurrentDataStore()
|
||||
const { locale } = useLocaleStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { setAvatars, avatars } = useUserDataStore()
|
||||
|
||||
const charRank = useMemo(() => {
|
||||
if (!avatarSelected) return null;
|
||||
const avatar = avatars[avatarSelected.ID];
|
||||
if (avatar?.enhanced != "") {
|
||||
return avatarSelected?.Enhanced?.[avatar?.enhanced]?.Ranks
|
||||
}
|
||||
return avatarSelected?.Ranks
|
||||
}, [avatarSelected, avatars]);
|
||||
|
||||
return (
|
||||
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
|
||||
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
|
||||
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
|
||||
{transI18n("eidolons")}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 m-4 p-4 font-bold gap-4 w-fit max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
|
||||
{charRank && avatars[avatarSelected?.ID || ""] && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Object.entries(charRank || {}).map(([key, rank]) => (
|
||||
<div key={key}
|
||||
className="flex flex-col items-center"
|
||||
|
||||
>
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={() => {
|
||||
let newRank = Number(rank.Rank)
|
||||
if (avatars[avatarSelected?.ID || ""]?.data?.rank == Number(rank.Rank)) {
|
||||
newRank = Number(rank.Rank) - 1
|
||||
}
|
||||
setAvatars({ ...avatars, [avatarSelected?.ID || ""]: { ...avatars[avatarSelected?.ID || ""], data: { ...avatars[avatarSelected?.ID || ""].data, rank: newRank } } })
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
className={`w-60 object-contain mb-2 ${Number(rank.Rank) <= avatars[avatarSelected?.ID.toString() || ""]?.data?.rank ? "" : "grayscale"}`}
|
||||
src={`${process.env.CDN_URL}/ui/ui3d/rank/_dependencies/textures/${avatarSelected?.ID}/${avatarSelected?.ID}_Rank_${rank.Rank}.png`}
|
||||
alt={`Rank ${rank.Rank} Image`}
|
||||
priority
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
width={240}
|
||||
height={240}
|
||||
/>
|
||||
|
||||
<div className="text-lg pb-1 flex items-center justify-items-center gap-2">
|
||||
<span className="inline-block text-indigo-500">{rank.Rank}.</span>
|
||||
<ParseText
|
||||
locale={locale}
|
||||
text={getLocaleName(locale, rank.Name)}
|
||||
className="text-center text-base font-normal leading-tight"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm font-normal">
|
||||
<div dangerouslySetInnerHTML={{ __html: replaceByParam(getLocaleName(locale, rank.Desc), rank.Param) }} />
|
||||
<ExtraEffectList extras={rank?.Extra} locale={locale} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,99 +1,99 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { replaceByParam, getLocaleName } from "@/helper"
|
||||
import { ExtraEffect } from "@/types"
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
type Props = {
|
||||
extras: Record<string, ExtraEffect> | undefined
|
||||
locale: string
|
||||
}
|
||||
|
||||
export default function ExtraEffectList({ extras, locale }: Props) {
|
||||
const [openList, setOpenList] = useState(false)
|
||||
const [openId, setOpenId] = useState<number | null>(null)
|
||||
const transI18n = useTranslations("DataPage")
|
||||
if (!extras || Object.keys(extras).length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer bg-primary/10 px-3 py-2 rounded-md"
|
||||
onClick={() => setOpenList(!openList)}
|
||||
>
|
||||
<span className="text-sm font-semibold text-primary">
|
||||
{transI18n("listExtraEffect")} ({Object.keys(extras).length})
|
||||
</span>
|
||||
|
||||
<span
|
||||
className={`transition-transform ${
|
||||
openList ? "rotate-90" : ""
|
||||
}`}
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ${
|
||||
openList ? "max-h-125 mt-2" : "max-h-0"
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{Object.values(extras).map((extra) => {
|
||||
const isOpen = openId === extra.ID
|
||||
|
||||
return (
|
||||
<div
|
||||
key={extra.ID}
|
||||
className="bg-primary/5 rounded-md"
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-between px-3 py-2 cursor-pointer"
|
||||
onClick={() =>
|
||||
setOpenId(isOpen ? null : extra.ID)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] uppercase font-bold bg-primary/50 px-1.5 py-0.5 rounded">
|
||||
{transI18n("extra")}
|
||||
</span>
|
||||
|
||||
<span className="text-sm font-medium text-primary/80">
|
||||
{getLocaleName(locale, extra.Name)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={`transition-transform ${
|
||||
isOpen ? "rotate-90" : ""
|
||||
}`}
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ${
|
||||
isOpen ? "max-h-40 px-3 pb-2" : "max-h-0"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="text-sm opacity-90"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, extra.Desc),
|
||||
extra.Param
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { replaceByParam, getLocaleName } from "@/helper"
|
||||
import { ExtraEffect } from "@/types"
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
type Props = {
|
||||
extras: Record<string, ExtraEffect> | undefined
|
||||
locale: string
|
||||
}
|
||||
|
||||
export default function ExtraEffectList({ extras, locale }: Props) {
|
||||
const [openList, setOpenList] = useState(false)
|
||||
const [openId, setOpenId] = useState<number | null>(null)
|
||||
const transI18n = useTranslations("DataPage")
|
||||
if (!extras || Object.keys(extras).length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
<div
|
||||
className="flex items-center justify-between cursor-pointer bg-primary/10 px-3 py-2 rounded-md"
|
||||
onClick={() => setOpenList(!openList)}
|
||||
>
|
||||
<span className="text-sm font-semibold text-primary">
|
||||
{transI18n("listExtraEffect")} ({Object.keys(extras).length})
|
||||
</span>
|
||||
|
||||
<span
|
||||
className={`transition-transform ${
|
||||
openList ? "rotate-90" : ""
|
||||
}`}
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ${
|
||||
openList ? "max-h-125 mt-2" : "max-h-0"
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{Object.values(extras).map((extra) => {
|
||||
const isOpen = openId === extra.ID
|
||||
|
||||
return (
|
||||
<div
|
||||
key={extra.ID}
|
||||
className="bg-primary/5 rounded-md"
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-between px-3 py-2 cursor-pointer"
|
||||
onClick={() =>
|
||||
setOpenId(isOpen ? null : extra.ID)
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] uppercase font-bold bg-primary/50 px-1.5 py-0.5 rounded">
|
||||
{transI18n("extra")}
|
||||
</span>
|
||||
|
||||
<span className="text-sm font-medium text-primary/80">
|
||||
{getLocaleName(locale, extra.Name)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<span
|
||||
className={`transition-transform ${
|
||||
isOpen ? "rotate-90" : ""
|
||||
}`}
|
||||
>
|
||||
▶
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ${
|
||||
isOpen ? "max-h-40 px-3 pb-2" : "max-h-0"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="text-sm opacity-90"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, extra.Desc),
|
||||
extra.Param
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,9 @@
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10">
|
||||
<aside>
|
||||
<p>Copyright © {new Date().getFullYear()} - Firefly Shelter (Powered by Nextjs & DaisyUi)</p>
|
||||
</aside>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10">
|
||||
<aside>
|
||||
<p>Copyright © {new Date().getFullYear()} - Firefly Shelter (Powered by Nextjs & DaisyUi)</p>
|
||||
</aside>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
+609
-609
File diff suppressed because it is too large
Load Diff
+265
-265
@@ -1,74 +1,74 @@
|
||||
"use client"
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { useState, useMemo } from "react";
|
||||
import useCopyProfileStore from "@/stores/copyProfile";
|
||||
import ProfileCard from "../card/profileCard";
|
||||
import { AvatarProfileCardType, AvatarProfileStore } from "@/types";
|
||||
import Image from "next/image";
|
||||
import { getNameChar, calcRarity } from "@/helper";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import { useTranslations } from "next-intl";
|
||||
import SelectCustomImage from "../select/customSelectImage";
|
||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
|
||||
export default function CopyImport() {
|
||||
const { avatars, setAvatar } = useUserDataStore();
|
||||
const { avatarSelected } = useCurrentDataStore()
|
||||
const { mapAvatar, baseType, damageType } = useDetailDataStore()
|
||||
const { locale } = useLocaleStore()
|
||||
const {
|
||||
selectedProfiles,
|
||||
avatarCopySelected,
|
||||
setSelectedProfiles,
|
||||
setAvatarCopySelected,
|
||||
listElement,
|
||||
listPath,
|
||||
listRank,
|
||||
setListElement,
|
||||
setListPath,
|
||||
setListRank
|
||||
} = useCopyProfileStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
|
||||
const [message, setMessage] = useState({
|
||||
type: "",
|
||||
text: ""
|
||||
})
|
||||
|
||||
const listAvatar = useMemo(() => {
|
||||
if (!mapAvatar || !locale || !transI18n) return []
|
||||
let list = Object.values(mapAvatar);
|
||||
const allElementFalse = !Object.values(listElement).some(v => v)
|
||||
const allPathFalse = !Object.values(listPath).some(v => v)
|
||||
const allRarityFalse = !Object.values(listRank).some(v => v)
|
||||
list = list.filter(item => (allElementFalse || listElement[item.DamageType]) && (allPathFalse || listPath[item.BaseType]) && (allRarityFalse || listRank[calcRarity(item.Rarity)]))
|
||||
list.sort((a, b) => {
|
||||
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
|
||||
if (r !== 0) return r
|
||||
return a.ID - b.ID
|
||||
})
|
||||
return list
|
||||
}, [mapAvatar, listElement, listPath, listRank, locale, transI18n])
|
||||
|
||||
|
||||
const handleProfileToggle = (profile: AvatarProfileCardType) => {
|
||||
if (selectedProfiles.some((selectedProfile) => selectedProfile.key === profile.key)) {
|
||||
setSelectedProfiles(selectedProfiles.filter((selectedProfile) => selectedProfile.key !== profile.key));
|
||||
return;
|
||||
}
|
||||
setSelectedProfiles([...selectedProfiles, profile]);
|
||||
};
|
||||
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedProfiles([]);
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("clearAll")
|
||||
})
|
||||
};
|
||||
|
||||
"use client"
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { useState, useMemo } from "react";
|
||||
import useCopyProfileStore from "@/stores/copyProfile";
|
||||
import ProfileCard from "../card/profileCard";
|
||||
import { AvatarProfileCardType, AvatarProfileStore } from "@/types";
|
||||
import Image from "next/image";
|
||||
import { getNameChar, calcRarity } from "@/helper";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import { useTranslations } from "next-intl";
|
||||
import SelectCustomImage from "../select/customSelectImage";
|
||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
|
||||
export default function CopyImport() {
|
||||
const { avatars, setAvatar } = useUserDataStore();
|
||||
const { avatarSelected } = useCurrentDataStore()
|
||||
const { mapAvatar, baseType, damageType } = useDetailDataStore()
|
||||
const { locale } = useLocaleStore()
|
||||
const {
|
||||
selectedProfiles,
|
||||
avatarCopySelected,
|
||||
setSelectedProfiles,
|
||||
setAvatarCopySelected,
|
||||
listElement,
|
||||
listPath,
|
||||
listRank,
|
||||
setListElement,
|
||||
setListPath,
|
||||
setListRank
|
||||
} = useCopyProfileStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
|
||||
const [message, setMessage] = useState({
|
||||
type: "",
|
||||
text: ""
|
||||
})
|
||||
|
||||
const listAvatar = useMemo(() => {
|
||||
if (!mapAvatar || !locale || !transI18n) return []
|
||||
let list = Object.values(mapAvatar);
|
||||
const allElementFalse = !Object.values(listElement).some(v => v)
|
||||
const allPathFalse = !Object.values(listPath).some(v => v)
|
||||
const allRarityFalse = !Object.values(listRank).some(v => v)
|
||||
list = list.filter(item => (allElementFalse || listElement[item.DamageType]) && (allPathFalse || listPath[item.BaseType]) && (allRarityFalse || listRank[calcRarity(item.Rarity)]))
|
||||
list.sort((a, b) => {
|
||||
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
|
||||
if (r !== 0) return r
|
||||
return a.ID - b.ID
|
||||
})
|
||||
return list
|
||||
}, [mapAvatar, listElement, listPath, listRank, locale, transI18n])
|
||||
|
||||
|
||||
const handleProfileToggle = (profile: AvatarProfileCardType) => {
|
||||
if (selectedProfiles.some((selectedProfile) => selectedProfile.key === profile.key)) {
|
||||
setSelectedProfiles(selectedProfiles.filter((selectedProfile) => selectedProfile.key !== profile.key));
|
||||
return;
|
||||
}
|
||||
setSelectedProfiles([...selectedProfiles, profile]);
|
||||
};
|
||||
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedProfiles([]);
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("clearAll")
|
||||
})
|
||||
};
|
||||
|
||||
const selectAll = () => {
|
||||
if (avatarCopySelected) {
|
||||
const sourceAvatar = avatars[avatarCopySelected.ID.toString()]
|
||||
@@ -79,32 +79,32 @@ export default function CopyImport() {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
key: index,
|
||||
...profile,
|
||||
} as AvatarProfileCardType
|
||||
}).filter((profile) => profile !== null));
|
||||
}
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("selectAll")
|
||||
})
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
if (selectedProfiles.length === 0) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: transI18n("pleaseSelectAtLeastOneProfile")
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!avatarCopySelected) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: transI18n("noAvatarToCopySelected")
|
||||
});
|
||||
return;
|
||||
}
|
||||
key: index,
|
||||
...profile,
|
||||
} as AvatarProfileCardType
|
||||
}).filter((profile) => profile !== null));
|
||||
}
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("selectAll")
|
||||
})
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
if (selectedProfiles.length === 0) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: transI18n("pleaseSelectAtLeastOneProfile")
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!avatarCopySelected) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: transI18n("noAvatarToCopySelected")
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!avatarSelected) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
@@ -130,180 +130,180 @@ export default function CopyImport() {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...profile,
|
||||
profile_name: profile.profile_name + ` - Copy: ${avatarCopySelected?.ID}`,
|
||||
} as AvatarProfileStore
|
||||
}).filter((profile) => profile !== null);
|
||||
...profile,
|
||||
profile_name: profile.profile_name + ` - Copy: ${avatarCopySelected?.ID}`,
|
||||
} as AvatarProfileStore
|
||||
}).filter((profile) => profile !== null);
|
||||
|
||||
const newAvatar = {
|
||||
...targetAvatar,
|
||||
profileList: targetAvatar.profileList.concat(newListProfile),
|
||||
profileSelect: targetAvatar.profileList.length + newListProfile.length - 1,
|
||||
}
|
||||
setAvatar(newAvatar);
|
||||
setSelectedProfiles([]);
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("copied")
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg px-2 min-h-[60vh]">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-2">
|
||||
|
||||
<div className="mt-2 w-full">
|
||||
|
||||
<div className="flex items-start flex-col gap-2 mt-2 w-full">
|
||||
<div>{transI18n("filter")}</div>
|
||||
<div className="flex flex-wrap justify-start gap-5 md:gap-10 mt-2 w-full">
|
||||
{/* Path */}
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2 justify-start items-center">
|
||||
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => {
|
||||
setListPath({ ...listPath, [key]: !listPath[key] })
|
||||
}}
|
||||
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||
alt={key}
|
||||
className="h-8 w-8 object-contain rounded-md"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Element */}
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2 justify-start items-center">
|
||||
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => {
|
||||
setListElement({ ...listElement, [key]: !listElement[key] })
|
||||
}}
|
||||
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: listElement[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||
alt={key}
|
||||
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rank */}
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2 justify-end items-center">
|
||||
{Object.entries(listRank).map(([key], index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setListRank({ ...listRank, [key]: !listRank[key] })
|
||||
}}
|
||||
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: listRank[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">{key}*</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
|
||||
{listAvatar.length > 0 && (
|
||||
<div>
|
||||
<div>{transI18n("characterName")}</div>
|
||||
<SelectCustomImage
|
||||
customSet={listAvatar.map((avatar) => ({
|
||||
value: avatar.ID.toString(),
|
||||
label: getNameChar(locale, transI18n, avatar),
|
||||
imageUrl: `${process.env.CDN_URL}/${avatar.Image.AvatarIconPath}`
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={avatarCopySelected?.ID.toString() || ""}
|
||||
placeholder="Character Select"
|
||||
setSelectedCustomSet={(value) => setAvatarCopySelected(mapAvatar[value] || null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Selection Info */}
|
||||
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
|
||||
<span className="text-blue-400 font-medium">
|
||||
{transI18n("selectedProfiles")}: {selectedProfiles.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
|
||||
>
|
||||
{transI18n("clearAll")}
|
||||
</button>
|
||||
<button
|
||||
onClick={selectAll}
|
||||
className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
|
||||
{transI18n("selectAll")}
|
||||
</button>
|
||||
{selectedProfiles.length > 0 && (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="text-success/75 hover:text-success text-sm font-medium px-3 py-1 border border-success/75 rounded hover:bg-success/10 transition-colors cursor-pointer">
|
||||
{transI18n("copy")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{message.type && message.text && (
|
||||
<div className={(message.type == "error" ? "text-error" : "text-success") + " text-lg mt-2"} >{message.type == "error" ? "😭" : "🎉"} {message.text}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Character Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
setAvatar(newAvatar);
|
||||
setSelectedProfiles([]);
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("copied")
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg px-2 min-h-[60vh]">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-2">
|
||||
|
||||
<div className="mt-2 w-full">
|
||||
|
||||
<div className="flex items-start flex-col gap-2 mt-2 w-full">
|
||||
<div>{transI18n("filter")}</div>
|
||||
<div className="flex flex-wrap justify-start gap-5 md:gap-10 mt-2 w-full">
|
||||
{/* Path */}
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2 justify-start items-center">
|
||||
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => {
|
||||
setListPath({ ...listPath, [key]: !listPath[key] })
|
||||
}}
|
||||
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||
alt={key}
|
||||
className="h-8 w-8 object-contain rounded-md"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Element */}
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2 justify-start items-center">
|
||||
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => {
|
||||
setListElement({ ...listElement, [key]: !listElement[key] })
|
||||
}}
|
||||
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: listElement[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||
alt={key}
|
||||
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rank */}
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2 justify-end items-center">
|
||||
{Object.entries(listRank).map(([key], index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setListRank({ ...listRank, [key]: !listRank[key] })
|
||||
}}
|
||||
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: listRank[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">{key}*</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
|
||||
{listAvatar.length > 0 && (
|
||||
<div>
|
||||
<div>{transI18n("characterName")}</div>
|
||||
<SelectCustomImage
|
||||
customSet={listAvatar.map((avatar) => ({
|
||||
value: avatar.ID.toString(),
|
||||
label: getNameChar(locale, transI18n, avatar),
|
||||
imageUrl: `${process.env.CDN_URL}/${avatar.Image.AvatarIconPath}`
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={avatarCopySelected?.ID.toString() || ""}
|
||||
placeholder="Character Select"
|
||||
setSelectedCustomSet={(value) => setAvatarCopySelected(mapAvatar[value] || null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Selection Info */}
|
||||
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
|
||||
<span className="text-blue-400 font-medium">
|
||||
{transI18n("selectedProfiles")}: {selectedProfiles.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
|
||||
>
|
||||
{transI18n("clearAll")}
|
||||
</button>
|
||||
<button
|
||||
onClick={selectAll}
|
||||
className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
|
||||
{transI18n("selectAll")}
|
||||
</button>
|
||||
{selectedProfiles.length > 0 && (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="text-success/75 hover:text-success text-sm font-medium px-3 py-1 border border-success/75 rounded hover:bg-success/10 transition-colors cursor-pointer">
|
||||
{transI18n("copy")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{message.type && message.text && (
|
||||
<div className={(message.type == "error" ? "text-error" : "text-success") + " text-lg mt-2"} >{message.type == "error" ? "😭" : "🎉"} {message.text}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Character Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{avatarCopySelected && avatars[avatarCopySelected?.ID.toString()]?.profileList.map((profile, index) => {
|
||||
if (!profile.lightcone?.item_id && Object.keys(profile.relics ?? {}).length == 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ProfileCard
|
||||
key={index}
|
||||
profile={{ ...profile, key: index }}
|
||||
selectedProfile={selectedProfiles}
|
||||
onProfileToggle={handleProfileToggle}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ProfileCard
|
||||
key={index}
|
||||
profile={{ ...profile, key: index }}
|
||||
selectedProfile={selectedProfiles}
|
||||
onProfileToggle={handleProfileToggle}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+124
-124
@@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
import { useState } from "react";
|
||||
import CharacterInfoCard from "../card/characterInfoCard";
|
||||
import useEnkaStore from "@/stores/enkaStore";
|
||||
import { SendDataThroughProxy } from "@/lib/api/api";
|
||||
import { CharacterInfoCardType, EnkaResponse } from "@/types";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
"use client"
|
||||
import { useState } from "react";
|
||||
import CharacterInfoCard from "../card/characterInfoCard";
|
||||
import useEnkaStore from "@/stores/enkaStore";
|
||||
import { SendDataThroughProxy } from "@/lib/api/api";
|
||||
import { CharacterInfoCardType, EnkaResponse } from "@/types";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { converterOneEnkaDataToAvatarStore } from "@/helper";
|
||||
import useModelStore from "@/stores/modelStore";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -31,24 +31,24 @@ const toCharacterInfoCard = (character: AvatarEnkaDetailInput): CharacterInfoCar
|
||||
relic_set_id: parseInt(relic.tid.toString().slice(1, -1), 10),
|
||||
})),
|
||||
});
|
||||
|
||||
export default function EnkaImport() {
|
||||
const {
|
||||
uidInput,
|
||||
setUidInput,
|
||||
enkaData,
|
||||
selectedCharacters,
|
||||
setSelectedCharacters,
|
||||
setEnkaData,
|
||||
} = useEnkaStore();
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { avatars, setAvatar } = useUserDataStore();
|
||||
|
||||
export default function EnkaImport() {
|
||||
const {
|
||||
uidInput,
|
||||
setUidInput,
|
||||
enkaData,
|
||||
selectedCharacters,
|
||||
setSelectedCharacters,
|
||||
setEnkaData,
|
||||
} = useEnkaStore();
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { avatars, setAvatar } = useUserDataStore();
|
||||
const { setIsOpenImport } = useModelStore()
|
||||
const { mapAvatar } = useDetailDataStore()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [Error, setError] = useState("")
|
||||
const validCharacters = enkaData?.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId]) ?? []
|
||||
|
||||
|
||||
const handlerFetchData = async () => {
|
||||
if (!uidInput) {
|
||||
setError(transI18n("pleaseEnterUid"))
|
||||
@@ -77,36 +77,36 @@ export default function EnkaImport() {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCharacterToggle = (character: CharacterInfoCardType) => {
|
||||
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
|
||||
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
|
||||
return;
|
||||
}
|
||||
setSelectedCharacters([...selectedCharacters, character]);
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedCharacters([]);
|
||||
};
|
||||
|
||||
|
||||
const handleCharacterToggle = (character: CharacterInfoCardType) => {
|
||||
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
|
||||
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
|
||||
return;
|
||||
}
|
||||
setSelectedCharacters([...selectedCharacters, character]);
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedCharacters([]);
|
||||
};
|
||||
|
||||
const selectAll = () => {
|
||||
if (enkaData) {
|
||||
setSelectedCharacters(validCharacters.map(toCharacterInfoCard));
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
if (selectedCharacters.length === 0) {
|
||||
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
|
||||
return;
|
||||
}
|
||||
if (!enkaData) {
|
||||
setError(transI18n("noDataToImport"));
|
||||
return;
|
||||
}
|
||||
setError("");
|
||||
const listAvatars = { ...avatars }
|
||||
|
||||
const handleImport = () => {
|
||||
if (selectedCharacters.length === 0) {
|
||||
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
|
||||
return;
|
||||
}
|
||||
if (!enkaData) {
|
||||
setError(transI18n("noDataToImport"));
|
||||
return;
|
||||
}
|
||||
setError("");
|
||||
const listAvatars = { ...avatars }
|
||||
const parsed = enkaResponseSchema.safeParse(enkaData)
|
||||
if (!parsed.success) {
|
||||
setError(transI18n("failedToFetchEnkaData"));
|
||||
@@ -125,80 +125,80 @@ export default function EnkaImport() {
|
||||
acc[skill.pointId] = skill.level;
|
||||
return acc;
|
||||
}, {} as Record<string, number>),
|
||||
}
|
||||
const newProfile = converterOneEnkaDataToAvatarStore(character, newAvatar.profileList.length)
|
||||
if (newProfile) {
|
||||
newAvatar.profileList.push(newProfile)
|
||||
newAvatar.profileSelect = newAvatar.profileList.length - 1
|
||||
}
|
||||
setAvatar(newAvatar)
|
||||
}
|
||||
|
||||
})
|
||||
setIsOpenImport(false)
|
||||
toast.success(transI18n("importEnkaDataSuccess"))
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-slate-900 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-white text-2xl font-bold mb-6">HSR UID</h1>
|
||||
|
||||
<div className="flex gap-4 items-center mb-6">
|
||||
<input
|
||||
type="text"
|
||||
value={uidInput}
|
||||
onChange={(e) => setUidInput(e.target.value)}
|
||||
className="bg-slate-800 text-white px-4 py-3 rounded-lg border border-slate-700 flex-1 max-w-md focus:outline-none focus:border-blue-500"
|
||||
placeholder="Enter UID"
|
||||
/>
|
||||
<button onClick={handlerFetchData} className="btn btn-success rounded-lg transition-colors">
|
||||
{transI18n("getData")}
|
||||
</button>
|
||||
{selectedCharacters.length > 0 && (
|
||||
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
|
||||
{transI18n("import")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{Error && (
|
||||
<div className="text-error text-base mt-2">😭 {Error}</div>
|
||||
)}
|
||||
|
||||
{/* Player Info */}
|
||||
{enkaData && (
|
||||
<div className="text-white space-y-2">
|
||||
<div className="text-lg">Name: {enkaData.detailInfo.nickname}</div>
|
||||
<div className="text-lg">UID: {enkaData.detailInfo.uid}</div>
|
||||
<div className="text-lg">Level: {enkaData.detailInfo.level}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selection Info */}
|
||||
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
|
||||
<span className="text-blue-400 font-medium">
|
||||
{transI18n("selectedCharacters")}: {selectedCharacters.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
|
||||
>
|
||||
{transI18n("clearAll")}
|
||||
</button>
|
||||
<button onClick={selectAll} className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
|
||||
{transI18n("selectAll")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
|
||||
</div>
|
||||
)}
|
||||
{/* Character Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
}
|
||||
const newProfile = converterOneEnkaDataToAvatarStore(character, newAvatar.profileList.length)
|
||||
if (newProfile) {
|
||||
newAvatar.profileList.push(newProfile)
|
||||
newAvatar.profileSelect = newAvatar.profileList.length - 1
|
||||
}
|
||||
setAvatar(newAvatar)
|
||||
}
|
||||
|
||||
})
|
||||
setIsOpenImport(false)
|
||||
toast.success(transI18n("importEnkaDataSuccess"))
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-slate-900 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-white text-2xl font-bold mb-6">HSR UID</h1>
|
||||
|
||||
<div className="flex gap-4 items-center mb-6">
|
||||
<input
|
||||
type="text"
|
||||
value={uidInput}
|
||||
onChange={(e) => setUidInput(e.target.value)}
|
||||
className="bg-slate-800 text-white px-4 py-3 rounded-lg border border-slate-700 flex-1 max-w-md focus:outline-none focus:border-blue-500"
|
||||
placeholder="Enter UID"
|
||||
/>
|
||||
<button onClick={handlerFetchData} className="btn btn-success rounded-lg transition-colors">
|
||||
{transI18n("getData")}
|
||||
</button>
|
||||
{selectedCharacters.length > 0 && (
|
||||
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
|
||||
{transI18n("import")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{Error && (
|
||||
<div className="text-error text-base mt-2">😭 {Error}</div>
|
||||
)}
|
||||
|
||||
{/* Player Info */}
|
||||
{enkaData && (
|
||||
<div className="text-white space-y-2">
|
||||
<div className="text-lg">Name: {enkaData.detailInfo.nickname}</div>
|
||||
<div className="text-lg">UID: {enkaData.detailInfo.uid}</div>
|
||||
<div className="text-lg">Level: {enkaData.detailInfo.level}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selection Info */}
|
||||
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
|
||||
<span className="text-blue-400 font-medium">
|
||||
{transI18n("selectedCharacters")}: {selectedCharacters.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
|
||||
>
|
||||
{transI18n("clearAll")}
|
||||
</button>
|
||||
<button onClick={selectAll} className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
|
||||
{transI18n("selectAll")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
|
||||
</div>
|
||||
)}
|
||||
{/* Character Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{validCharacters.map((character) => (
|
||||
<CharacterInfoCard
|
||||
key={character.avatarId}
|
||||
@@ -206,9 +206,9 @@ export default function EnkaImport() {
|
||||
selectedCharacters={selectedCharacters}
|
||||
onCharacterToggle={handleCharacterToggle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+134
-134
@@ -1,15 +1,15 @@
|
||||
"use client"
|
||||
|
||||
import useFreeSRStore from "@/stores/freesrStore";
|
||||
import useModelStore from "@/stores/modelStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { CharacterInfoCardType } from "@/types";
|
||||
import { useState } from "react";
|
||||
import CharacterInfoCard from "../card/characterInfoCard";
|
||||
import { freeSrJsonSchema } from "@/zod";
|
||||
import { toast } from "react-toastify";
|
||||
import { converterOneFreeSRDataToAvatarStore } from "@/helper";
|
||||
import { useTranslations } from "next-intl";
|
||||
"use client"
|
||||
|
||||
import useFreeSRStore from "@/stores/freesrStore";
|
||||
import useModelStore from "@/stores/modelStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { CharacterInfoCardType } from "@/types";
|
||||
import { useState } from "react";
|
||||
import CharacterInfoCard from "../card/characterInfoCard";
|
||||
import { freeSrJsonSchema } from "@/zod";
|
||||
import { toast } from "react-toastify";
|
||||
import { converterOneFreeSRDataToAvatarStore } from "@/helper";
|
||||
import { useTranslations } from "next-intl";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -36,55 +36,55 @@ const toCharacterInfoCard = (data: FreeSRJsonInput, character: FreeSRJsonInput["
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export default function FreeSRImport() {
|
||||
const { avatars, setAvatar } = useUserDataStore();
|
||||
const { mapAvatar } = useDetailDataStore();
|
||||
const { setIsOpenImport } = useModelStore()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
export default function FreeSRImport() {
|
||||
const { avatars, setAvatar } = useUserDataStore();
|
||||
const { mapAvatar } = useDetailDataStore();
|
||||
const { setIsOpenImport } = useModelStore()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [Error, setError] = useState("")
|
||||
const { freeSRData, setFreeSRData, selectedCharacters, setSelectedCharacters } = useFreeSRStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const parsedFreeSRData = freeSrJsonSchema.safeParse(freeSRData)
|
||||
const validFreeSRData = parsedFreeSRData.success ? parsedFreeSRData.data : null
|
||||
|
||||
const handleCharacterToggle = (character: CharacterInfoCardType) => {
|
||||
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
|
||||
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
|
||||
return;
|
||||
}
|
||||
setSelectedCharacters([...selectedCharacters, character]);
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedCharacters([]);
|
||||
};
|
||||
|
||||
|
||||
const handleCharacterToggle = (character: CharacterInfoCardType) => {
|
||||
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
|
||||
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
|
||||
return;
|
||||
}
|
||||
setSelectedCharacters([...selectedCharacters, character]);
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedCharacters([]);
|
||||
};
|
||||
|
||||
const selectAll = () => {
|
||||
const parsed = freeSrJsonSchema.safeParse(freeSRData)
|
||||
if (parsed.success) {
|
||||
setSelectedCharacters(Object.values(parsed.data.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => toCharacterInfoCard(parsed.data, character)));
|
||||
}
|
||||
};
|
||||
|
||||
const handlerReadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsLoading(true)
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) {
|
||||
setSelectedCharacters([])
|
||||
setFreeSRData(null)
|
||||
setError(transI18n("pleaseSelectAFile"))
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
if (!file.name.endsWith(".json") || file.type !== "application/json") {
|
||||
setSelectedCharacters([])
|
||||
setFreeSRData(null)
|
||||
setError(transI18n("fileMustBeAValidJsonFile"))
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const handlerReadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsLoading(true)
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) {
|
||||
setSelectedCharacters([])
|
||||
setFreeSRData(null)
|
||||
setError(transI18n("pleaseSelectAFile"))
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
if (!file.name.endsWith(".json") || file.type !== "application/json") {
|
||||
setSelectedCharacters([])
|
||||
setFreeSRData(null)
|
||||
setError(transI18n("fileMustBeAValidJsonFile"))
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (file) {
|
||||
|
||||
const reader = new FileReader();
|
||||
@@ -121,18 +121,18 @@ export default function FreeSRImport() {
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
if (selectedCharacters.length === 0) {
|
||||
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
|
||||
return;
|
||||
}
|
||||
if (!freeSRData) {
|
||||
setError(transI18n("noDataToImport"));
|
||||
return;
|
||||
}
|
||||
setError("");
|
||||
|
||||
|
||||
const handleImport = () => {
|
||||
if (selectedCharacters.length === 0) {
|
||||
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
|
||||
return;
|
||||
}
|
||||
if (!freeSRData) {
|
||||
setError(transI18n("noDataToImport"));
|
||||
return;
|
||||
}
|
||||
setError("");
|
||||
|
||||
const parsed = freeSrJsonSchema.safeParse(freeSRData)
|
||||
if (!parsed.success) {
|
||||
setError(transI18n("fileMustBeAValidJsonFile"));
|
||||
@@ -144,73 +144,73 @@ export default function FreeSRImport() {
|
||||
filterData.forEach((character) => {
|
||||
const newAvatar = { ...listAvatars[character.avatar_id] }
|
||||
if (Object.keys(newAvatar).length !== 0) {
|
||||
newAvatar.level = character.level
|
||||
newAvatar.promotion = character.promotion
|
||||
newAvatar.data = {
|
||||
rank: character.data.rank ?? 0,
|
||||
skills: character.data.skills
|
||||
}
|
||||
newAvatar.level = character.level
|
||||
newAvatar.promotion = character.promotion
|
||||
newAvatar.data = {
|
||||
rank: character.data.rank ?? 0,
|
||||
skills: character.data.skills
|
||||
}
|
||||
const newProfile = converterOneFreeSRDataToAvatarStore(parsed.data, newAvatar.profileList.length, character.avatar_id)
|
||||
if (newProfile) {
|
||||
newAvatar.profileList.push(newProfile)
|
||||
newAvatar.profileSelect = newAvatar.profileList.length - 1
|
||||
}
|
||||
setAvatar(newAvatar)
|
||||
}
|
||||
|
||||
})
|
||||
setIsOpenImport(false)
|
||||
toast.success(transI18n("importFreeSRDataSuccess"))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-slate-900 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-white text-2xl font-bold mb-6">{transI18n("freeSRImport")}</h1>
|
||||
|
||||
<div className="flex gap-4 items-center mb-6">
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">{transI18n("pickAFile")}</legend>
|
||||
<input type="file" accept=".json" className="file-input file-input-info" onChange={handlerReadFile} />
|
||||
<label className="label">{transI18n("onlySupportFreeSRJsonFile")}</label>
|
||||
</fieldset>
|
||||
|
||||
{selectedCharacters.length > 0 && (
|
||||
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
|
||||
{transI18n("import")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{Error && (
|
||||
<div className="text-error text-base mt-2">😭 {Error}</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Selection Info */}
|
||||
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
|
||||
<span className="text-blue-400 font-medium">
|
||||
{transI18n("selectedCharacters")}: {selectedCharacters.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
|
||||
>
|
||||
{transI18n("clearAll")}
|
||||
</button>
|
||||
<button onClick={selectAll} className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
|
||||
{transI18n("selectAll")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
|
||||
</div>
|
||||
)}
|
||||
{/* Character Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
if (newProfile) {
|
||||
newAvatar.profileList.push(newProfile)
|
||||
newAvatar.profileSelect = newAvatar.profileList.length - 1
|
||||
}
|
||||
setAvatar(newAvatar)
|
||||
}
|
||||
|
||||
})
|
||||
setIsOpenImport(false)
|
||||
toast.success(transI18n("importFreeSRDataSuccess"))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-slate-900 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-white text-2xl font-bold mb-6">{transI18n("freeSRImport")}</h1>
|
||||
|
||||
<div className="flex gap-4 items-center mb-6">
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">{transI18n("pickAFile")}</legend>
|
||||
<input type="file" accept=".json" className="file-input file-input-info" onChange={handlerReadFile} />
|
||||
<label className="label">{transI18n("onlySupportFreeSRJsonFile")}</label>
|
||||
</fieldset>
|
||||
|
||||
{selectedCharacters.length > 0 && (
|
||||
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
|
||||
{transI18n("import")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{Error && (
|
||||
<div className="text-error text-base mt-2">😭 {Error}</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Selection Info */}
|
||||
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
|
||||
<span className="text-blue-400 font-medium">
|
||||
{transI18n("selectedCharacters")}: {selectedCharacters.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
|
||||
>
|
||||
{transI18n("clearAll")}
|
||||
</button>
|
||||
<button onClick={selectAll} className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
|
||||
{transI18n("selectAll")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
|
||||
</div>
|
||||
)}
|
||||
{/* Character Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{validFreeSRData && Object.values(validFreeSRData.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => {
|
||||
return (
|
||||
<CharacterInfoCard
|
||||
@@ -219,10 +219,10 @@ export default function FreeSRImport() {
|
||||
selectedCharacters={selectedCharacters}
|
||||
onCharacterToggle={handleCharacterToggle}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,145 +1,145 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo } from "react"
|
||||
import Image from "next/image";
|
||||
import useLocaleStore from "@/stores/localeStore"
|
||||
import LightconeCard from "../card/lightconeCard";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import useModelStore from "@/stores/modelStore";
|
||||
import { useTranslations } from "next-intl";
|
||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
import { calcRarity, getLocaleName } from "@/helper";
|
||||
|
||||
export default function LightconeBar() {
|
||||
const { locale } = useLocaleStore()
|
||||
const {
|
||||
avatarSelected,
|
||||
mapLightconePathActive,
|
||||
mapLightconeRankActive,
|
||||
setMapLightconePathActive,
|
||||
setMapLightconeRankActive,
|
||||
lightconeSearch,
|
||||
setLightconeSearch
|
||||
} = useCurrentDataStore()
|
||||
const { setAvatar, avatars } = useUserDataStore()
|
||||
const { setIsOpenLightcone } = useModelStore()
|
||||
const { mapLightCone, baseType } = useDetailDataStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
|
||||
const listLightcone = useMemo(() => {
|
||||
if (!mapLightCone || !locale) return []
|
||||
|
||||
let list = Object.values(mapLightCone)
|
||||
|
||||
if (lightconeSearch) {
|
||||
list = list.filter(item => getLocaleName(locale, item.Name).toLowerCase().includes(lightconeSearch.toLowerCase()))
|
||||
}
|
||||
|
||||
const allRankFalse = !Object.values(mapLightconeRankActive).some(v => v)
|
||||
const allPathFalse = !Object.values(mapLightconePathActive).some(v => v)
|
||||
|
||||
list = list.filter(item =>
|
||||
(allRankFalse || mapLightconeRankActive[item.Rarity]) &&
|
||||
(allPathFalse || mapLightconePathActive[item.BaseType])
|
||||
)
|
||||
|
||||
list.sort((a, b) => {
|
||||
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
|
||||
if (r !== 0) return r
|
||||
return b.ID - a.ID
|
||||
})
|
||||
|
||||
return list
|
||||
}, [mapLightCone, mapLightconePathActive, mapLightconeRankActive, lightconeSearch, locale])
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
||||
{transI18n("lightConeSetting")}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="mt-2 w-full">
|
||||
<div className="flex items-start flex-col gap-2">
|
||||
<div>Search</div>
|
||||
<input
|
||||
value={lightconeSearch}
|
||||
onChange={(e) => setLightconeSearch(e.target.value)}
|
||||
type="text" placeholder="LightCone Name" className="input input-accent mt-1 w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start flex-col gap-2 mt-2 w-full">
|
||||
<div>Filter</div>
|
||||
<div className="flex flex-row flex-wrap justify-between mt-1 w-full">
|
||||
<div className="flex flex-wrap mb-1 mx-1 gap-2">
|
||||
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => {
|
||||
setMapLightconePathActive({ ...mapLightconePathActive, [key]: !mapLightconePathActive[key] })
|
||||
}}
|
||||
className="h-9.5 w-9.5 md:h-12.5 md:w-12.5 hover:bg-gray-600 grid place-items-center rounded-md shadow-lg cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: mapLightconePathActive[key] ? "#374151" : "#6B7280"
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||
alt={key}
|
||||
className="h-7 w-7 md:h-8 md:w-8 object-contain rounded-md"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap mb-1 mx-1 gap-2">
|
||||
{Object.keys(mapLightconeRankActive).map((key, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setMapLightconeRankActive({ ...mapLightconeRankActive, [key]: !mapLightconeRankActive[key] })
|
||||
}}
|
||||
className="h-9.5 w-9.5 md:h-12.5 md:w-12.5 hover:bg-gray-600 grid place-items-center rounded-md shadow-lg cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: mapLightconeRankActive[key] ? "#374151" : "#6B7280"
|
||||
}}
|
||||
>
|
||||
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">
|
||||
{key}*
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-8 mt-2 gap-2">
|
||||
{listLightcone.map((item, index) => (
|
||||
<div key={index} onClick={() => {
|
||||
if (avatarSelected) {
|
||||
const avatar = avatars[avatarSelected?.ID?.toString()]
|
||||
avatar.profileList[avatar.profileSelect].lightcone = {
|
||||
level: 80,
|
||||
item_id: item.ID,
|
||||
rank: 1,
|
||||
promotion: 6
|
||||
}
|
||||
setAvatar({ ...avatar })
|
||||
setIsOpenLightcone(false)
|
||||
}
|
||||
}}>
|
||||
<LightconeCard data={item} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
"use client"
|
||||
|
||||
import { useMemo } from "react"
|
||||
import Image from "next/image";
|
||||
import useLocaleStore from "@/stores/localeStore"
|
||||
import LightconeCard from "../card/lightconeCard";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import useModelStore from "@/stores/modelStore";
|
||||
import { useTranslations } from "next-intl";
|
||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
import { calcRarity, getLocaleName } from "@/helper";
|
||||
|
||||
export default function LightconeBar() {
|
||||
const { locale } = useLocaleStore()
|
||||
const {
|
||||
avatarSelected,
|
||||
mapLightconePathActive,
|
||||
mapLightconeRankActive,
|
||||
setMapLightconePathActive,
|
||||
setMapLightconeRankActive,
|
||||
lightconeSearch,
|
||||
setLightconeSearch
|
||||
} = useCurrentDataStore()
|
||||
const { setAvatar, avatars } = useUserDataStore()
|
||||
const { setIsOpenLightcone } = useModelStore()
|
||||
const { mapLightCone, baseType } = useDetailDataStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
|
||||
const listLightcone = useMemo(() => {
|
||||
if (!mapLightCone || !locale) return []
|
||||
|
||||
let list = Object.values(mapLightCone)
|
||||
|
||||
if (lightconeSearch) {
|
||||
list = list.filter(item => getLocaleName(locale, item.Name).toLowerCase().includes(lightconeSearch.toLowerCase()))
|
||||
}
|
||||
|
||||
const allRankFalse = !Object.values(mapLightconeRankActive).some(v => v)
|
||||
const allPathFalse = !Object.values(mapLightconePathActive).some(v => v)
|
||||
|
||||
list = list.filter(item =>
|
||||
(allRankFalse || mapLightconeRankActive[item.Rarity]) &&
|
||||
(allPathFalse || mapLightconePathActive[item.BaseType])
|
||||
)
|
||||
|
||||
list.sort((a, b) => {
|
||||
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
|
||||
if (r !== 0) return r
|
||||
return b.ID - a.ID
|
||||
})
|
||||
|
||||
return list
|
||||
}, [mapLightCone, mapLightconePathActive, mapLightconeRankActive, lightconeSearch, locale])
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
||||
{transI18n("lightConeSetting")}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="mt-2 w-full">
|
||||
<div className="flex items-start flex-col gap-2">
|
||||
<div>Search</div>
|
||||
<input
|
||||
value={lightconeSearch}
|
||||
onChange={(e) => setLightconeSearch(e.target.value)}
|
||||
type="text" placeholder="LightCone Name" className="input input-accent mt-1 w-full"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start flex-col gap-2 mt-2 w-full">
|
||||
<div>Filter</div>
|
||||
<div className="flex flex-row flex-wrap justify-between mt-1 w-full">
|
||||
<div className="flex flex-wrap mb-1 mx-1 gap-2">
|
||||
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => {
|
||||
setMapLightconePathActive({ ...mapLightconePathActive, [key]: !mapLightconePathActive[key] })
|
||||
}}
|
||||
className="h-9.5 w-9.5 md:h-12.5 md:w-12.5 hover:bg-gray-600 grid place-items-center rounded-md shadow-lg cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: mapLightconePathActive[key] ? "#374151" : "#6B7280"
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||
alt={key}
|
||||
className="h-7 w-7 md:h-8 md:w-8 object-contain rounded-md"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap mb-1 mx-1 gap-2">
|
||||
{Object.keys(mapLightconeRankActive).map((key, index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setMapLightconeRankActive({ ...mapLightconeRankActive, [key]: !mapLightconeRankActive[key] })
|
||||
}}
|
||||
className="h-9.5 w-9.5 md:h-12.5 md:w-12.5 hover:bg-gray-600 grid place-items-center rounded-md shadow-lg cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: mapLightconeRankActive[key] ? "#374151" : "#6B7280"
|
||||
}}
|
||||
>
|
||||
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">
|
||||
{key}*
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-8 mt-2 gap-2">
|
||||
{listLightcone.map((item, index) => (
|
||||
<div key={index} onClick={() => {
|
||||
if (avatarSelected) {
|
||||
const avatar = avatars[avatarSelected?.ID?.toString()]
|
||||
avatar.profileList[avatar.profileSelect].lightcone = {
|
||||
level: 80,
|
||||
item_id: item.ID,
|
||||
rank: 1,
|
||||
promotion: 6
|
||||
}
|
||||
setAvatar({ ...avatar })
|
||||
setIsOpenLightcone(false)
|
||||
}
|
||||
}}>
|
||||
<LightconeCard data={item} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+340
-340
@@ -1,341 +1,341 @@
|
||||
"use client"
|
||||
import { useEffect, useMemo } from "react";
|
||||
import SelectCustomText from "../select/customSelectText";
|
||||
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import Image from "next/image";
|
||||
import { ASEvent, MonsterStore } from "@/types";
|
||||
import { useTranslations } from "next-intl";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
|
||||
export default function AsBar() {
|
||||
const { locale } = useLocaleStore()
|
||||
const {
|
||||
as_config,
|
||||
setAsConfig
|
||||
} = useUserDataStore()
|
||||
const { mapMonster, mapAS, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
|
||||
const challengeSelected = useMemo(() => {
|
||||
return mapAS[as_config.event_id.toString()]?.Level.find((as) => as.ID === as_config.challenge_id)
|
||||
}, [as_config, mapAS])
|
||||
|
||||
const eventSelected = useMemo(() => {
|
||||
return mapAS[as_config.event_id.toString()]
|
||||
}, [as_config, mapAS])
|
||||
|
||||
|
||||
const floorSideList = useMemo(() => {
|
||||
if (!eventSelected) return [];
|
||||
|
||||
const floorList = [
|
||||
{
|
||||
id: "firstNode",
|
||||
name: transI18n("firstNode"),
|
||||
wave: transI18n("firstNodeEnemies")
|
||||
},
|
||||
{
|
||||
id: "secondNode",
|
||||
name: transI18n("secondNode"),
|
||||
wave: transI18n("secondNodeEnemies")
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
if (eventSelected?.Tierce && eventSelected.Tierce.PreChallenge === as_config.challenge_id) {
|
||||
floorList.push({
|
||||
id: "thirdNode",
|
||||
name: transI18n("thirdNode"),
|
||||
wave: transI18n("thirdNodeEnemies")
|
||||
})
|
||||
}
|
||||
return floorList
|
||||
}, [as_config.challenge_id, eventSelected, transI18n])
|
||||
|
||||
const buffList = useMemo(() => {
|
||||
if (!eventSelected) return [];
|
||||
|
||||
if (as_config.floor_side === "firstNode") {
|
||||
return eventSelected?.BuffList1 ?? [];
|
||||
}
|
||||
|
||||
if (as_config.floor_side === "secondNode") {
|
||||
return eventSelected?.BuffList2 ?? [];
|
||||
}
|
||||
|
||||
if (as_config.floor_side === "thirdNode" && eventSelected?.BuffList3) {
|
||||
return eventSelected?.BuffList3 ?? [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [as_config.floor_side, eventSelected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!challengeSelected || as_config.event_id === 0 || as_config.challenge_id === 0) return
|
||||
const newBattleConfig = structuredClone(as_config)
|
||||
newBattleConfig.cycle_count = challengeSelected.TurnLimit
|
||||
|
||||
newBattleConfig.blessings = []
|
||||
if (as_config.buff_id !== 0) {
|
||||
newBattleConfig.blessings.push({
|
||||
id: as_config.buff_id,
|
||||
level: 1
|
||||
})
|
||||
}
|
||||
|
||||
if (challengeSelected) {
|
||||
challengeSelected.MazeBuff.map((item) => {
|
||||
newBattleConfig.blessings.push({
|
||||
id: item.ID,
|
||||
level: 1
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
newBattleConfig.monsters = []
|
||||
newBattleConfig.stage_id = 0
|
||||
|
||||
let targetEventList: ASEvent[] = []
|
||||
if (as_config.floor_side === "firstNode" && challengeSelected.EventList1.length > 0) {
|
||||
targetEventList = challengeSelected.EventList1
|
||||
} else if (as_config.floor_side === "secondNode" && challengeSelected.EventList2.length > 0) {
|
||||
targetEventList = challengeSelected.EventList2
|
||||
} else if (as_config.floor_side === "thirdNode" && eventSelected?.Tierce && eventSelected.Tierce.EventList.length > 0) {
|
||||
targetEventList = eventSelected.Tierce.EventList
|
||||
}
|
||||
|
||||
if (targetEventList.length > 0) {
|
||||
newBattleConfig.stage_id = targetEventList[0].ID
|
||||
for (const wave of targetEventList[0].MonsterList) {
|
||||
const newWave: MonsterStore[] = []
|
||||
for (const value of Object.values(wave)) {
|
||||
newWave.push({
|
||||
monster_id: value,
|
||||
level: targetEventList[0].Level,
|
||||
amount: 1,
|
||||
})
|
||||
}
|
||||
newBattleConfig.monsters.push(newWave)
|
||||
}
|
||||
}
|
||||
|
||||
setAsConfig(newBattleConfig)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
challengeSelected,
|
||||
eventSelected,
|
||||
mapAS,
|
||||
as_config.event_id,
|
||||
as_config.challenge_id,
|
||||
as_config.floor_side,
|
||||
as_config.buff_id,
|
||||
])
|
||||
|
||||
if (!mapAS) return null
|
||||
return (
|
||||
<div className="py-8 relative">
|
||||
|
||||
{/* Title Card */}
|
||||
<div className="rounded-xl p-4 mb-2 border border-warning">
|
||||
<div className="mb-4 w-full">
|
||||
<SelectCustomText
|
||||
customSet={Object.values(mapAS).sort((a, b) => b.ID - a.ID).map((as) => ({
|
||||
id: as.ID.toString(),
|
||||
name: getLocaleName(locale, as.Name),
|
||||
time: `${as.BeginTime} - ${as.EndTime}`,
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={as_config.event_id.toString()}
|
||||
placeholder={transI18n("selectASEvent")}
|
||||
setSelectedCustomSet={(id) => setAsConfig({
|
||||
...as_config,
|
||||
event_id: Number(id),
|
||||
challenge_id: mapAS[Number(id)]?.Level.at(-1)?.ID || 0,
|
||||
buff_id: 0
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{/* Settings */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-2">
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
|
||||
</label>
|
||||
<select
|
||||
value={as_config.challenge_id}
|
||||
className="select select-success"
|
||||
onChange={(e) => setAsConfig({ ...as_config, challenge_id: Number(e.target.value) })}
|
||||
>
|
||||
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
|
||||
{eventSelected?.Level.map((as) => (
|
||||
<option key={as.ID} value={as.ID}>{getLocaleName(locale, as.Name)}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
|
||||
</label>
|
||||
<select
|
||||
value={as_config.floor_side}
|
||||
className="select select-success"
|
||||
onChange={(e) => setAsConfig({ ...as_config, floor_side: e.target.value })}
|
||||
>
|
||||
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
|
||||
{floorSideList.map((side) => {
|
||||
return <option key={side.id} value={side.id}>{side.name}</option>
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="label-text font-bold text-success mb-2">StageId: {as_config?.stage_id}</div>
|
||||
{eventSelected && (
|
||||
<div className="mb-4 w-full">
|
||||
<SelectCustomText
|
||||
customSet={buffList.map((buff) => ({
|
||||
id: buff.ID?.toString() || "",
|
||||
name: getLocaleName(locale, buff?.Name) || "",
|
||||
description: replaceByParam(getLocaleName(locale, buff?.Desc) || "", buff?.Param || []),
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={as_config?.buff_id?.toString()}
|
||||
placeholder={transI18n("selectBuff")}
|
||||
setSelectedCustomSet={(id) => setAsConfig({ ...as_config, buff_id: Number(id) })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* Turbulence Buff */}
|
||||
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
|
||||
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2>
|
||||
{challengeSelected ? (
|
||||
challengeSelected.MazeBuff.map((buff, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-base"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, buff?.Desc) || "",
|
||||
buff?.Param || []
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enemy Waves */}
|
||||
{(as_config?.challenge_id ?? 0) !== 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{floorSideList.map((side, i) => {
|
||||
const eventList = side.id === "firstNode"
|
||||
? challengeSelected?.EventList1
|
||||
: side.id === "secondNode"
|
||||
? challengeSelected?.EventList2
|
||||
: side.id === "thirdNode"
|
||||
? eventSelected?.Tierce?.EventList
|
||||
: [];
|
||||
|
||||
if (!eventList || eventList.length === 0) return null;
|
||||
const targetEvent = eventList[0];
|
||||
|
||||
return (
|
||||
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
|
||||
<h2 className="text-2xl font-bold mb-6 text-info">{side.wave}</h2>
|
||||
|
||||
{targetEvent?.MonsterList?.map((wave, waveIndex) => (
|
||||
<div key={waveIndex} className="mb-6">
|
||||
<h3 className="text-lg font-semibold">{transI18n("wave")} {waveIndex + 1}</h3>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{Object.values(wave).map((waveValue, enemyIndex) => {
|
||||
const monsterStats = calcMonsterStats(
|
||||
mapMonster?.[waveValue.toString()],
|
||||
targetEvent?.EliteGroup,
|
||||
targetEvent?.HardLevelGroup,
|
||||
targetEvent?.Level,
|
||||
hardLevelConfig,
|
||||
eliteConfig
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={enemyIndex}
|
||||
className="group relative flex flex-col w-40 bg-base-100 rounded-2xl border border-base-300 shadow-md"
|
||||
>
|
||||
<div className="badge badge-warning badge-sm font-bold absolute top-2 right-2 z-10 shadow-sm">
|
||||
Lv. {targetEvent.Level}
|
||||
</div>
|
||||
|
||||
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
|
||||
{mapMonster?.[waveValue.toString()]?.Image?.IconPath && (
|
||||
<div className="relative w-16 h-16 rounded-full border-2 border-base-300 shadow-md overflow-hidden group-hover:scale-110 transition-transform duration-300 bg-base-100">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${mapMonster?.[waveValue.toString()]?.Image?.IconPath}`}
|
||||
alt="Enemy Icon"
|
||||
width={150}
|
||||
height={150}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col px-1 pb-2 pt-2">
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-error">HP</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.hp.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-info">Speed</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.spd.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-base-content/70">Toughness</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.stance.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 pt-2 border-t border-base-300 flex flex-col items-center">
|
||||
<span className="text-[10px] text-base-content/60 font-bold uppercase tracking-widest mb-1.5">
|
||||
Weakness
|
||||
</span>
|
||||
<div className="flex items-center justify-center gap-1.5 flex-wrap">
|
||||
{mapMonster?.[waveValue.toString()]?.StanceWeakList?.map((icon, iconIndex) => (
|
||||
<Image
|
||||
key={iconIndex}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${damageType[icon]?.Icon}`}
|
||||
alt={icon}
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-6 w-6 object-contain rounded-full bg-base-300 border border-base-content/10 p-0.5 shadow-sm hover:scale-110 transition-transform"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
"use client"
|
||||
import { useEffect, useMemo } from "react";
|
||||
import SelectCustomText from "../select/customSelectText";
|
||||
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import Image from "next/image";
|
||||
import { ASEvent, MonsterStore } from "@/types";
|
||||
import { useTranslations } from "next-intl";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
|
||||
export default function AsBar() {
|
||||
const { locale } = useLocaleStore()
|
||||
const {
|
||||
as_config,
|
||||
setAsConfig
|
||||
} = useUserDataStore()
|
||||
const { mapMonster, mapAS, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
|
||||
const challengeSelected = useMemo(() => {
|
||||
return mapAS[as_config.event_id.toString()]?.Level.find((as) => as.ID === as_config.challenge_id)
|
||||
}, [as_config, mapAS])
|
||||
|
||||
const eventSelected = useMemo(() => {
|
||||
return mapAS[as_config.event_id.toString()]
|
||||
}, [as_config, mapAS])
|
||||
|
||||
|
||||
const floorSideList = useMemo(() => {
|
||||
if (!eventSelected) return [];
|
||||
|
||||
const floorList = [
|
||||
{
|
||||
id: "firstNode",
|
||||
name: transI18n("firstNode"),
|
||||
wave: transI18n("firstNodeEnemies")
|
||||
},
|
||||
{
|
||||
id: "secondNode",
|
||||
name: transI18n("secondNode"),
|
||||
wave: transI18n("secondNodeEnemies")
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
if (eventSelected?.Tierce && eventSelected.Tierce.PreChallenge === as_config.challenge_id) {
|
||||
floorList.push({
|
||||
id: "thirdNode",
|
||||
name: transI18n("thirdNode"),
|
||||
wave: transI18n("thirdNodeEnemies")
|
||||
})
|
||||
}
|
||||
return floorList
|
||||
}, [as_config.challenge_id, eventSelected, transI18n])
|
||||
|
||||
const buffList = useMemo(() => {
|
||||
if (!eventSelected) return [];
|
||||
|
||||
if (as_config.floor_side === "firstNode") {
|
||||
return eventSelected?.BuffList1 ?? [];
|
||||
}
|
||||
|
||||
if (as_config.floor_side === "secondNode") {
|
||||
return eventSelected?.BuffList2 ?? [];
|
||||
}
|
||||
|
||||
if (as_config.floor_side === "thirdNode" && eventSelected?.BuffList3) {
|
||||
return eventSelected?.BuffList3 ?? [];
|
||||
}
|
||||
|
||||
return [];
|
||||
}, [as_config.floor_side, eventSelected]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!challengeSelected || as_config.event_id === 0 || as_config.challenge_id === 0) return
|
||||
const newBattleConfig = structuredClone(as_config)
|
||||
newBattleConfig.cycle_count = challengeSelected.TurnLimit
|
||||
|
||||
newBattleConfig.blessings = []
|
||||
if (as_config.buff_id !== 0) {
|
||||
newBattleConfig.blessings.push({
|
||||
id: as_config.buff_id,
|
||||
level: 1
|
||||
})
|
||||
}
|
||||
|
||||
if (challengeSelected) {
|
||||
challengeSelected.MazeBuff.map((item) => {
|
||||
newBattleConfig.blessings.push({
|
||||
id: item.ID,
|
||||
level: 1
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
newBattleConfig.monsters = []
|
||||
newBattleConfig.stage_id = 0
|
||||
|
||||
let targetEventList: ASEvent[] = []
|
||||
if (as_config.floor_side === "firstNode" && challengeSelected.EventList1.length > 0) {
|
||||
targetEventList = challengeSelected.EventList1
|
||||
} else if (as_config.floor_side === "secondNode" && challengeSelected.EventList2.length > 0) {
|
||||
targetEventList = challengeSelected.EventList2
|
||||
} else if (as_config.floor_side === "thirdNode" && eventSelected?.Tierce && eventSelected.Tierce.EventList.length > 0) {
|
||||
targetEventList = eventSelected.Tierce.EventList
|
||||
}
|
||||
|
||||
if (targetEventList.length > 0) {
|
||||
newBattleConfig.stage_id = targetEventList[0].ID
|
||||
for (const wave of targetEventList[0].MonsterList) {
|
||||
const newWave: MonsterStore[] = []
|
||||
for (const value of Object.values(wave)) {
|
||||
newWave.push({
|
||||
monster_id: value,
|
||||
level: targetEventList[0].Level,
|
||||
amount: 1,
|
||||
})
|
||||
}
|
||||
newBattleConfig.monsters.push(newWave)
|
||||
}
|
||||
}
|
||||
|
||||
setAsConfig(newBattleConfig)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
challengeSelected,
|
||||
eventSelected,
|
||||
mapAS,
|
||||
as_config.event_id,
|
||||
as_config.challenge_id,
|
||||
as_config.floor_side,
|
||||
as_config.buff_id,
|
||||
])
|
||||
|
||||
if (!mapAS) return null
|
||||
return (
|
||||
<div className="py-8 relative">
|
||||
|
||||
{/* Title Card */}
|
||||
<div className="rounded-xl p-4 mb-2 border border-warning">
|
||||
<div className="mb-4 w-full">
|
||||
<SelectCustomText
|
||||
customSet={Object.values(mapAS).sort((a, b) => b.ID - a.ID).map((as) => ({
|
||||
id: as.ID.toString(),
|
||||
name: getLocaleName(locale, as.Name),
|
||||
time: `${as.BeginTime} - ${as.EndTime}`,
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={as_config.event_id.toString()}
|
||||
placeholder={transI18n("selectASEvent")}
|
||||
setSelectedCustomSet={(id) => setAsConfig({
|
||||
...as_config,
|
||||
event_id: Number(id),
|
||||
challenge_id: mapAS[Number(id)]?.Level.at(-1)?.ID || 0,
|
||||
buff_id: 0
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{/* Settings */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-2">
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
|
||||
</label>
|
||||
<select
|
||||
value={as_config.challenge_id}
|
||||
className="select select-success"
|
||||
onChange={(e) => setAsConfig({ ...as_config, challenge_id: Number(e.target.value) })}
|
||||
>
|
||||
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
|
||||
{eventSelected?.Level.map((as) => (
|
||||
<option key={as.ID} value={as.ID}>{getLocaleName(locale, as.Name)}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
|
||||
</label>
|
||||
<select
|
||||
value={as_config.floor_side}
|
||||
className="select select-success"
|
||||
onChange={(e) => setAsConfig({ ...as_config, floor_side: e.target.value })}
|
||||
>
|
||||
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
|
||||
{floorSideList.map((side) => {
|
||||
return <option key={side.id} value={side.id}>{side.name}</option>
|
||||
})}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="label-text font-bold text-success mb-2">StageId: {as_config?.stage_id}</div>
|
||||
{eventSelected && (
|
||||
<div className="mb-4 w-full">
|
||||
<SelectCustomText
|
||||
customSet={buffList.map((buff) => ({
|
||||
id: buff.ID?.toString() || "",
|
||||
name: getLocaleName(locale, buff?.Name) || "",
|
||||
description: replaceByParam(getLocaleName(locale, buff?.Desc) || "", buff?.Param || []),
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={as_config?.buff_id?.toString()}
|
||||
placeholder={transI18n("selectBuff")}
|
||||
setSelectedCustomSet={(id) => setAsConfig({ ...as_config, buff_id: Number(id) })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* Turbulence Buff */}
|
||||
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
|
||||
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2>
|
||||
{challengeSelected ? (
|
||||
challengeSelected.MazeBuff.map((buff, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-base"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, buff?.Desc) || "",
|
||||
buff?.Param || []
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enemy Waves */}
|
||||
{(as_config?.challenge_id ?? 0) !== 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{floorSideList.map((side, i) => {
|
||||
const eventList = side.id === "firstNode"
|
||||
? challengeSelected?.EventList1
|
||||
: side.id === "secondNode"
|
||||
? challengeSelected?.EventList2
|
||||
: side.id === "thirdNode"
|
||||
? eventSelected?.Tierce?.EventList
|
||||
: [];
|
||||
|
||||
if (!eventList || eventList.length === 0) return null;
|
||||
const targetEvent = eventList[0];
|
||||
|
||||
return (
|
||||
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
|
||||
<h2 className="text-2xl font-bold mb-6 text-info">{side.wave}</h2>
|
||||
|
||||
{targetEvent?.MonsterList?.map((wave, waveIndex) => (
|
||||
<div key={waveIndex} className="mb-6">
|
||||
<h3 className="text-lg font-semibold">{transI18n("wave")} {waveIndex + 1}</h3>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{Object.values(wave).map((waveValue, enemyIndex) => {
|
||||
const monsterStats = calcMonsterStats(
|
||||
mapMonster?.[waveValue.toString()],
|
||||
targetEvent?.EliteGroup,
|
||||
targetEvent?.HardLevelGroup,
|
||||
targetEvent?.Level,
|
||||
hardLevelConfig,
|
||||
eliteConfig
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={enemyIndex}
|
||||
className="group relative flex flex-col w-40 bg-base-100 rounded-2xl border border-base-300 shadow-md"
|
||||
>
|
||||
<div className="badge badge-warning badge-sm font-bold absolute top-2 right-2 z-10 shadow-sm">
|
||||
Lv. {targetEvent.Level}
|
||||
</div>
|
||||
|
||||
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
|
||||
{mapMonster?.[waveValue.toString()]?.Image?.IconPath && (
|
||||
<div className="relative w-16 h-16 rounded-full border-2 border-base-300 shadow-md overflow-hidden group-hover:scale-110 transition-transform duration-300 bg-base-100">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${mapMonster?.[waveValue.toString()]?.Image?.IconPath}`}
|
||||
alt="Enemy Icon"
|
||||
width={150}
|
||||
height={150}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col px-1 pb-2 pt-2">
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-error">HP</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.hp.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-info">Speed</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.spd.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-base-content/70">Toughness</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.stance.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 pt-2 border-t border-base-300 flex flex-col items-center">
|
||||
<span className="text-[10px] text-base-content/60 font-bold uppercase tracking-widest mb-1.5">
|
||||
Weakness
|
||||
</span>
|
||||
<div className="flex items-center justify-center gap-1.5 flex-wrap">
|
||||
{mapMonster?.[waveValue.toString()]?.StanceWeakList?.map((icon, iconIndex) => (
|
||||
<Image
|
||||
key={iconIndex}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${damageType[icon]?.Icon}`}
|
||||
alt={icon}
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-6 w-6 object-contain rounded-full bg-base-300 border border-base-content/10 p-0.5 shadow-sm hover:scale-110 transition-transform"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+533
-533
File diff suppressed because it is too large
Load Diff
@@ -1,100 +1,100 @@
|
||||
import MocBar from "./moc";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import PfBar from "./pf";
|
||||
import Image from "next/image";
|
||||
import AsBar from "./as";
|
||||
import { useTranslations } from "next-intl";
|
||||
import CeBar from "./ce";
|
||||
import PeakBar from "./peak";
|
||||
|
||||
export default function MonsterBar() {
|
||||
const { battle_type, setBattleType } = useUserDataStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
|
||||
const navItems = [
|
||||
{ name: transI18n("memoryOfChaos"), icon: 'AbyssIcon01', value: 'MOC' },
|
||||
{ name: transI18n("pureFiction"), icon: 'ChallengeStory', value: 'PF' },
|
||||
{ name: transI18n("apocalypticShadow"), icon: 'ChallengeBoss', value: 'AS' },
|
||||
{ name: transI18n("anomalyArbitration"), icon: 'ChallengePeakIcon', value: 'PEAK' },
|
||||
{ name: transI18n("customEnemy"), icon: 'MonsterIcon', value: 'CE' },
|
||||
{ name: transI18n("simulatedUniverse"), icon: 'SimulatedUniverse', value: 'SU' },
|
||||
|
||||
];
|
||||
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
{/* Header Navigation */}
|
||||
<nav className="border-b border-warning/30 relative pb-2">
|
||||
<div className="flex items-center justify-center">
|
||||
{/* Mobile Select */}
|
||||
<div className="block md:hidden w-full">
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
value={battle_type.toUpperCase()}
|
||||
onChange={(e) => setBattleType(e.target.value.toUpperCase())}
|
||||
>
|
||||
{navItems.map((item) => (
|
||||
<option key={item.name} value={item.value.toUpperCase()}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Desktop Tabs */}
|
||||
<div className="hidden md:grid grid-cols-3 lg:grid-cols-6 gap-1">
|
||||
{navItems.map((item) => (
|
||||
<button
|
||||
key={item.name}
|
||||
onClick={() => setBattleType(item.value.toUpperCase())}
|
||||
className={`px-4 py-2 rounded-lg transition-all cursor-pointer duration-300 flex items-center space-x-2 ${battle_type.toUpperCase() === item.value.toUpperCase()
|
||||
? 'bg-success/30 shadow-lg'
|
||||
: 'bg-base-200/20 hover:bg-base-200/40 '
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
style={
|
||||
battle_type.toUpperCase() === item.value.toUpperCase()
|
||||
? {
|
||||
filter:
|
||||
'brightness(0) saturate(100%) invert(63%) sepia(78%) saturate(643%) hue-rotate(1deg) brightness(93%) contrast(89%)',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`/icon/${item.icon}.webp`}
|
||||
alt={item.name}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</span>
|
||||
<span>{item.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{(battle_type.toUpperCase() === "DEFAULT" || battle_type.toUpperCase() === "") && (
|
||||
<div className="container mx-auto px-4 py-8 text-center font-bold text-3xl">
|
||||
{transI18n("noEventSelected")}
|
||||
</div>
|
||||
)}
|
||||
{battle_type.toUpperCase() === 'MOC' && <MocBar />}
|
||||
{battle_type.toUpperCase() === 'PF' && <PfBar />}
|
||||
{battle_type.toUpperCase() === 'AS' && <AsBar />}
|
||||
{battle_type.toUpperCase() === 'CE' && <CeBar />}
|
||||
{battle_type.toUpperCase() === 'PEAK' && <PeakBar />}
|
||||
{battle_type.toUpperCase() === 'SU' && (
|
||||
<div className="container mx-auto px-4 py-8 text-center font-bold text-3xl">
|
||||
{transI18n("comingSoon")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
import MocBar from "./moc";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import PfBar from "./pf";
|
||||
import Image from "next/image";
|
||||
import AsBar from "./as";
|
||||
import { useTranslations } from "next-intl";
|
||||
import CeBar from "./ce";
|
||||
import PeakBar from "./peak";
|
||||
|
||||
export default function MonsterBar() {
|
||||
const { battle_type, setBattleType } = useUserDataStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
|
||||
const navItems = [
|
||||
{ name: transI18n("memoryOfChaos"), icon: 'AbyssIcon01', value: 'MOC' },
|
||||
{ name: transI18n("pureFiction"), icon: 'ChallengeStory', value: 'PF' },
|
||||
{ name: transI18n("apocalypticShadow"), icon: 'ChallengeBoss', value: 'AS' },
|
||||
{ name: transI18n("anomalyArbitration"), icon: 'ChallengePeakIcon', value: 'PEAK' },
|
||||
{ name: transI18n("customEnemy"), icon: 'MonsterIcon', value: 'CE' },
|
||||
{ name: transI18n("simulatedUniverse"), icon: 'SimulatedUniverse', value: 'SU' },
|
||||
|
||||
];
|
||||
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
{/* Header Navigation */}
|
||||
<nav className="border-b border-warning/30 relative pb-2">
|
||||
<div className="flex items-center justify-center">
|
||||
{/* Mobile Select */}
|
||||
<div className="block md:hidden w-full">
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
value={battle_type.toUpperCase()}
|
||||
onChange={(e) => setBattleType(e.target.value.toUpperCase())}
|
||||
>
|
||||
{navItems.map((item) => (
|
||||
<option key={item.name} value={item.value.toUpperCase()}>
|
||||
{item.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Desktop Tabs */}
|
||||
<div className="hidden md:grid grid-cols-3 lg:grid-cols-6 gap-1">
|
||||
{navItems.map((item) => (
|
||||
<button
|
||||
key={item.name}
|
||||
onClick={() => setBattleType(item.value.toUpperCase())}
|
||||
className={`px-4 py-2 rounded-lg transition-all cursor-pointer duration-300 flex items-center space-x-2 ${battle_type.toUpperCase() === item.value.toUpperCase()
|
||||
? 'bg-success/30 shadow-lg'
|
||||
: 'bg-base-200/20 hover:bg-base-200/40 '
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
style={
|
||||
battle_type.toUpperCase() === item.value.toUpperCase()
|
||||
? {
|
||||
filter:
|
||||
'brightness(0) saturate(100%) invert(63%) sepia(78%) saturate(643%) hue-rotate(1deg) brightness(93%) contrast(89%)',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`/icon/${item.icon}.webp`}
|
||||
alt={item.name}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</span>
|
||||
<span>{item.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{(battle_type.toUpperCase() === "DEFAULT" || battle_type.toUpperCase() === "") && (
|
||||
<div className="container mx-auto px-4 py-8 text-center font-bold text-3xl">
|
||||
{transI18n("noEventSelected")}
|
||||
</div>
|
||||
)}
|
||||
{battle_type.toUpperCase() === 'MOC' && <MocBar />}
|
||||
{battle_type.toUpperCase() === 'PF' && <PfBar />}
|
||||
{battle_type.toUpperCase() === 'AS' && <AsBar />}
|
||||
{battle_type.toUpperCase() === 'CE' && <CeBar />}
|
||||
{battle_type.toUpperCase() === 'PEAK' && <PeakBar />}
|
||||
{battle_type.toUpperCase() === 'SU' && (
|
||||
<div className="container mx-auto px-4 py-8 text-center font-bold text-3xl">
|
||||
{transI18n("comingSoon")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+341
-341
@@ -1,342 +1,342 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useMemo } from "react";
|
||||
import SelectCustomText from "../select/customSelectText";
|
||||
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { MoCEvent, MonsterStore } from "@/types";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
|
||||
export default function MocBar() {
|
||||
const { locale } = useLocaleStore()
|
||||
const {
|
||||
moc_config,
|
||||
setMocConfig
|
||||
} = useUserDataStore()
|
||||
const { mapMonster, mapMoc, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
|
||||
|
||||
const transI18n = useTranslations("DataPage")
|
||||
|
||||
const challengeSelected = useMemo(() => {
|
||||
return mapMoc[moc_config.event_id.toString()]?.Level.find((moc) => moc.ID === moc_config.challenge_id)
|
||||
}, [moc_config, mapMoc])
|
||||
|
||||
const eventSelected = useMemo(() => {
|
||||
return mapMoc[moc_config.event_id.toString()]
|
||||
}, [moc_config, mapMoc])
|
||||
|
||||
const floorSideList = useMemo(() => {
|
||||
if (!eventSelected) return [];
|
||||
|
||||
const floorList = [
|
||||
{
|
||||
id: "firstNode",
|
||||
name: transI18n("firstNode"),
|
||||
wave: transI18n("firstNodeEnemies")
|
||||
},
|
||||
{
|
||||
id: "secondNode",
|
||||
name: transI18n("secondNode"),
|
||||
wave: transI18n("secondNodeEnemies")
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
if (eventSelected?.Tierce && eventSelected.Tierce.PreChallenge === moc_config.challenge_id) {
|
||||
floorList.push({
|
||||
id: "thirdNode",
|
||||
name: transI18n("thirdNode"),
|
||||
wave: transI18n("thirdNodeEnemies")
|
||||
})
|
||||
}
|
||||
return floorList
|
||||
}, [moc_config.challenge_id, eventSelected, transI18n])
|
||||
|
||||
useEffect(() => {
|
||||
if (!challengeSelected || moc_config.event_id === 0 || moc_config.challenge_id === 0) return
|
||||
|
||||
const newBattleConfig = structuredClone(moc_config)
|
||||
newBattleConfig.cycle_count = 0
|
||||
if (moc_config.use_cycle_count) {
|
||||
newBattleConfig.cycle_count = challengeSelected.TurnLimit
|
||||
}
|
||||
newBattleConfig.blessings = []
|
||||
if (moc_config.use_turbulence_buff && challengeSelected) {
|
||||
challengeSelected.MazeBuff.map((item) => {
|
||||
newBattleConfig.blessings.push({
|
||||
id: item.ID,
|
||||
level: 1
|
||||
})
|
||||
})
|
||||
}
|
||||
newBattleConfig.monsters = []
|
||||
newBattleConfig.stage_id = 0
|
||||
|
||||
let targetEventList: MoCEvent[] = []
|
||||
if (moc_config.floor_side === "firstNode" && challengeSelected.EventList1.length > 0) {
|
||||
targetEventList = challengeSelected.EventList1
|
||||
} else if (moc_config.floor_side === "secondNode" && challengeSelected.EventList2.length > 0) {
|
||||
targetEventList = challengeSelected.EventList2
|
||||
} else if (moc_config.floor_side === "thirdNode" && eventSelected?.Tierce && eventSelected.Tierce.EventList.length > 0) {
|
||||
targetEventList = eventSelected.Tierce.EventList
|
||||
}
|
||||
|
||||
if (targetEventList.length > 0) {
|
||||
newBattleConfig.stage_id = targetEventList[0].ID
|
||||
for (const wave of targetEventList[0].MonsterList) {
|
||||
const newWave: MonsterStore[] = []
|
||||
for (const value of Object.values(wave)) {
|
||||
newWave.push({
|
||||
monster_id: value,
|
||||
level: targetEventList[0].Level,
|
||||
amount: 1,
|
||||
})
|
||||
}
|
||||
newBattleConfig.monsters.push(newWave)
|
||||
}
|
||||
}
|
||||
|
||||
setMocConfig(newBattleConfig)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
challengeSelected,
|
||||
eventSelected,
|
||||
moc_config.event_id,
|
||||
moc_config.challenge_id,
|
||||
moc_config.floor_side,
|
||||
moc_config.use_cycle_count,
|
||||
moc_config.use_turbulence_buff,
|
||||
mapMoc,
|
||||
])
|
||||
|
||||
if (!mapMoc) return null
|
||||
|
||||
return (
|
||||
<div className="py-8 relative">
|
||||
|
||||
{/* Title Card */}
|
||||
<div className="rounded-xl p-4 mb-2 border border-warning">
|
||||
<div className="mb-4 w-full">
|
||||
<SelectCustomText
|
||||
customSet={Object.values(mapMoc).sort((a, b) => b.ID - a.ID).map((moc) => ({
|
||||
id: moc.ID.toString(),
|
||||
name: getLocaleName(locale, moc.Name),
|
||||
time: `${moc.BeginTime} - ${moc.EndTime}`,
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={moc_config.event_id.toString()}
|
||||
placeholder={transI18n("selectMOCEvent")}
|
||||
setSelectedCustomSet={(id) => setMocConfig({
|
||||
...moc_config,
|
||||
event_id: Number(id),
|
||||
challenge_id: mapMoc[Number(id)]?.Level.at(-1)?.ID || 0,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{/* Settings */}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-2">
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
|
||||
</label>
|
||||
<select
|
||||
value={moc_config.challenge_id}
|
||||
className="select select-success"
|
||||
onChange={(e) => setMocConfig({
|
||||
...moc_config,
|
||||
challenge_id: Number(e.target.value)
|
||||
})}
|
||||
>
|
||||
<option value={0} disabled={true}>Select a Floor</option>
|
||||
{eventSelected?.Level?.map((moc) => (
|
||||
<option key={moc.ID} value={moc.ID}>{getLocaleName(locale, moc.Name)}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
|
||||
</label>
|
||||
<select
|
||||
value={moc_config.floor_side}
|
||||
className="select select-success"
|
||||
onChange={(e) => setMocConfig({ ...moc_config, floor_side: e.target.value })}
|
||||
>
|
||||
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
|
||||
{floorSideList.map((side) => (
|
||||
<option key={side.id} value={side.id}>{side.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="label cursor-pointer">
|
||||
<span
|
||||
onClick={() => setMocConfig({ ...moc_config, use_cycle_count: !moc_config.use_cycle_count })}
|
||||
className="label-text font-bold text-success cursor-pointer"
|
||||
>
|
||||
{transI18n("useCycleCount")} {" "}
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={moc_config.use_cycle_count}
|
||||
onChange={(e) => setMocConfig({ ...moc_config, use_cycle_count: e.target.checked })}
|
||||
className="checkbox checkbox-primary"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="label-text font-bold text-success mb-2">StageId: {moc_config?.stage_id}</div>
|
||||
|
||||
{/* Turbulence Buff */}
|
||||
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={moc_config.use_turbulence_buff}
|
||||
onChange={(e) => setMocConfig({ ...moc_config, use_turbulence_buff: e.target.checked })}
|
||||
className="checkbox checkbox-primary"
|
||||
/>
|
||||
<span
|
||||
onClick={() => setMocConfig({ ...moc_config, use_turbulence_buff: !moc_config.use_turbulence_buff })}
|
||||
className="font-bold text-success cursor-pointer">
|
||||
{transI18n("useTurbulenceBuff")}
|
||||
</span>
|
||||
</div>
|
||||
{challengeSelected ? (
|
||||
challengeSelected.MazeBuff.map((buff, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-base"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, buff?.Desc) || "",
|
||||
buff?.Param || []
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enemy Waves */}
|
||||
{(moc_config?.challenge_id ?? 0) !== 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{floorSideList.map((side, i) => {
|
||||
const eventList = side.id === "firstNode"
|
||||
? challengeSelected?.EventList1
|
||||
: side.id === "secondNode"
|
||||
? challengeSelected?.EventList2
|
||||
: side.id === "thirdNode"
|
||||
? eventSelected?.Tierce?.EventList
|
||||
: [];
|
||||
|
||||
if (!eventList || eventList.length === 0) return null;
|
||||
const targetEvent = eventList[0];
|
||||
|
||||
return (
|
||||
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
|
||||
<h2 className="text-2xl font-bold mb-6 text-info">{side.wave}</h2>
|
||||
|
||||
{targetEvent?.MonsterList?.map((wave, waveIndex) => (
|
||||
<div key={waveIndex} className="mb-6">
|
||||
<h3 className="text-lg font-semibold">{transI18n("wave")} {waveIndex + 1}</h3>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{Object.values(wave).map((waveValue, enemyIndex) => {
|
||||
const monsterStats = calcMonsterStats(
|
||||
mapMonster?.[waveValue.toString()],
|
||||
targetEvent?.EliteGroup,
|
||||
targetEvent?.HardLevelGroup,
|
||||
targetEvent?.Level,
|
||||
hardLevelConfig,
|
||||
eliteConfig
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={enemyIndex}
|
||||
className="group relative flex flex-col w-40 bg-base-100 rounded-2xl border border-base-300 shadow-md"
|
||||
>
|
||||
<div className="badge badge-warning badge-sm font-bold absolute top-2 right-2 z-10 shadow-sm">
|
||||
Lv. {targetEvent.Level}
|
||||
</div>
|
||||
|
||||
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
|
||||
{mapMonster?.[waveValue.toString()]?.Image?.IconPath && (
|
||||
<div className="relative w-16 h-16 rounded-full border-2 border-base-300 shadow-md overflow-hidden group-hover:scale-110 transition-transform duration-300 bg-base-100">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${mapMonster?.[waveValue.toString()]?.Image?.IconPath}`}
|
||||
alt="Enemy Icon"
|
||||
width={150}
|
||||
height={150}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col px-1 pb-2 pt-2">
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-error">HP</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.hp.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-info">Speed</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.spd.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-base-content/70">Toughness</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.stance.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 pt-2 border-t border-base-300 flex flex-col items-center">
|
||||
<span className="text-[10px] text-base-content/60 font-bold uppercase tracking-widest mb-1.5">
|
||||
Weakness
|
||||
</span>
|
||||
<div className="flex items-center justify-center gap-1.5 flex-wrap">
|
||||
{mapMonster?.[waveValue.toString()]?.StanceWeakList?.map((icon, iconIndex) => (
|
||||
<Image
|
||||
key={iconIndex}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${damageType[icon]?.Icon}`}
|
||||
alt={icon}
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-6 w-6 object-contain rounded-full bg-base-300 border border-base-content/10 p-0.5 shadow-sm hover:scale-110 transition-transform"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
"use client"
|
||||
|
||||
import { useEffect, useMemo } from "react";
|
||||
import SelectCustomText from "../select/customSelectText";
|
||||
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { MoCEvent, MonsterStore } from "@/types";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
|
||||
export default function MocBar() {
|
||||
const { locale } = useLocaleStore()
|
||||
const {
|
||||
moc_config,
|
||||
setMocConfig
|
||||
} = useUserDataStore()
|
||||
const { mapMonster, mapMoc, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
|
||||
|
||||
const transI18n = useTranslations("DataPage")
|
||||
|
||||
const challengeSelected = useMemo(() => {
|
||||
return mapMoc[moc_config.event_id.toString()]?.Level.find((moc) => moc.ID === moc_config.challenge_id)
|
||||
}, [moc_config, mapMoc])
|
||||
|
||||
const eventSelected = useMemo(() => {
|
||||
return mapMoc[moc_config.event_id.toString()]
|
||||
}, [moc_config, mapMoc])
|
||||
|
||||
const floorSideList = useMemo(() => {
|
||||
if (!eventSelected) return [];
|
||||
|
||||
const floorList = [
|
||||
{
|
||||
id: "firstNode",
|
||||
name: transI18n("firstNode"),
|
||||
wave: transI18n("firstNodeEnemies")
|
||||
},
|
||||
{
|
||||
id: "secondNode",
|
||||
name: transI18n("secondNode"),
|
||||
wave: transI18n("secondNodeEnemies")
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
if (eventSelected?.Tierce && eventSelected.Tierce.PreChallenge === moc_config.challenge_id) {
|
||||
floorList.push({
|
||||
id: "thirdNode",
|
||||
name: transI18n("thirdNode"),
|
||||
wave: transI18n("thirdNodeEnemies")
|
||||
})
|
||||
}
|
||||
return floorList
|
||||
}, [moc_config.challenge_id, eventSelected, transI18n])
|
||||
|
||||
useEffect(() => {
|
||||
if (!challengeSelected || moc_config.event_id === 0 || moc_config.challenge_id === 0) return
|
||||
|
||||
const newBattleConfig = structuredClone(moc_config)
|
||||
newBattleConfig.cycle_count = 0
|
||||
if (moc_config.use_cycle_count) {
|
||||
newBattleConfig.cycle_count = challengeSelected.TurnLimit
|
||||
}
|
||||
newBattleConfig.blessings = []
|
||||
if (moc_config.use_turbulence_buff && challengeSelected) {
|
||||
challengeSelected.MazeBuff.map((item) => {
|
||||
newBattleConfig.blessings.push({
|
||||
id: item.ID,
|
||||
level: 1
|
||||
})
|
||||
})
|
||||
}
|
||||
newBattleConfig.monsters = []
|
||||
newBattleConfig.stage_id = 0
|
||||
|
||||
let targetEventList: MoCEvent[] = []
|
||||
if (moc_config.floor_side === "firstNode" && challengeSelected.EventList1.length > 0) {
|
||||
targetEventList = challengeSelected.EventList1
|
||||
} else if (moc_config.floor_side === "secondNode" && challengeSelected.EventList2.length > 0) {
|
||||
targetEventList = challengeSelected.EventList2
|
||||
} else if (moc_config.floor_side === "thirdNode" && eventSelected?.Tierce && eventSelected.Tierce.EventList.length > 0) {
|
||||
targetEventList = eventSelected.Tierce.EventList
|
||||
}
|
||||
|
||||
if (targetEventList.length > 0) {
|
||||
newBattleConfig.stage_id = targetEventList[0].ID
|
||||
for (const wave of targetEventList[0].MonsterList) {
|
||||
const newWave: MonsterStore[] = []
|
||||
for (const value of Object.values(wave)) {
|
||||
newWave.push({
|
||||
monster_id: value,
|
||||
level: targetEventList[0].Level,
|
||||
amount: 1,
|
||||
})
|
||||
}
|
||||
newBattleConfig.monsters.push(newWave)
|
||||
}
|
||||
}
|
||||
|
||||
setMocConfig(newBattleConfig)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
challengeSelected,
|
||||
eventSelected,
|
||||
moc_config.event_id,
|
||||
moc_config.challenge_id,
|
||||
moc_config.floor_side,
|
||||
moc_config.use_cycle_count,
|
||||
moc_config.use_turbulence_buff,
|
||||
mapMoc,
|
||||
])
|
||||
|
||||
if (!mapMoc) return null
|
||||
|
||||
return (
|
||||
<div className="py-8 relative">
|
||||
|
||||
{/* Title Card */}
|
||||
<div className="rounded-xl p-4 mb-2 border border-warning">
|
||||
<div className="mb-4 w-full">
|
||||
<SelectCustomText
|
||||
customSet={Object.values(mapMoc).sort((a, b) => b.ID - a.ID).map((moc) => ({
|
||||
id: moc.ID.toString(),
|
||||
name: getLocaleName(locale, moc.Name),
|
||||
time: `${moc.BeginTime} - ${moc.EndTime}`,
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={moc_config.event_id.toString()}
|
||||
placeholder={transI18n("selectMOCEvent")}
|
||||
setSelectedCustomSet={(id) => setMocConfig({
|
||||
...moc_config,
|
||||
event_id: Number(id),
|
||||
challenge_id: mapMoc[Number(id)]?.Level.at(-1)?.ID || 0,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{/* Settings */}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-2">
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
|
||||
</label>
|
||||
<select
|
||||
value={moc_config.challenge_id}
|
||||
className="select select-success"
|
||||
onChange={(e) => setMocConfig({
|
||||
...moc_config,
|
||||
challenge_id: Number(e.target.value)
|
||||
})}
|
||||
>
|
||||
<option value={0} disabled={true}>Select a Floor</option>
|
||||
{eventSelected?.Level?.map((moc) => (
|
||||
<option key={moc.ID} value={moc.ID}>{getLocaleName(locale, moc.Name)}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
|
||||
</label>
|
||||
<select
|
||||
value={moc_config.floor_side}
|
||||
className="select select-success"
|
||||
onChange={(e) => setMocConfig({ ...moc_config, floor_side: e.target.value })}
|
||||
>
|
||||
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
|
||||
{floorSideList.map((side) => (
|
||||
<option key={side.id} value={side.id}>{side.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="label cursor-pointer">
|
||||
<span
|
||||
onClick={() => setMocConfig({ ...moc_config, use_cycle_count: !moc_config.use_cycle_count })}
|
||||
className="label-text font-bold text-success cursor-pointer"
|
||||
>
|
||||
{transI18n("useCycleCount")} {" "}
|
||||
</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={moc_config.use_cycle_count}
|
||||
onChange={(e) => setMocConfig({ ...moc_config, use_cycle_count: e.target.checked })}
|
||||
className="checkbox checkbox-primary"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="label-text font-bold text-success mb-2">StageId: {moc_config?.stage_id}</div>
|
||||
|
||||
{/* Turbulence Buff */}
|
||||
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={moc_config.use_turbulence_buff}
|
||||
onChange={(e) => setMocConfig({ ...moc_config, use_turbulence_buff: e.target.checked })}
|
||||
className="checkbox checkbox-primary"
|
||||
/>
|
||||
<span
|
||||
onClick={() => setMocConfig({ ...moc_config, use_turbulence_buff: !moc_config.use_turbulence_buff })}
|
||||
className="font-bold text-success cursor-pointer">
|
||||
{transI18n("useTurbulenceBuff")}
|
||||
</span>
|
||||
</div>
|
||||
{challengeSelected ? (
|
||||
challengeSelected.MazeBuff.map((buff, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-base"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, buff?.Desc) || "",
|
||||
buff?.Param || []
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enemy Waves */}
|
||||
{(moc_config?.challenge_id ?? 0) !== 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{floorSideList.map((side, i) => {
|
||||
const eventList = side.id === "firstNode"
|
||||
? challengeSelected?.EventList1
|
||||
: side.id === "secondNode"
|
||||
? challengeSelected?.EventList2
|
||||
: side.id === "thirdNode"
|
||||
? eventSelected?.Tierce?.EventList
|
||||
: [];
|
||||
|
||||
if (!eventList || eventList.length === 0) return null;
|
||||
const targetEvent = eventList[0];
|
||||
|
||||
return (
|
||||
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
|
||||
<h2 className="text-2xl font-bold mb-6 text-info">{side.wave}</h2>
|
||||
|
||||
{targetEvent?.MonsterList?.map((wave, waveIndex) => (
|
||||
<div key={waveIndex} className="mb-6">
|
||||
<h3 className="text-lg font-semibold">{transI18n("wave")} {waveIndex + 1}</h3>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{Object.values(wave).map((waveValue, enemyIndex) => {
|
||||
const monsterStats = calcMonsterStats(
|
||||
mapMonster?.[waveValue.toString()],
|
||||
targetEvent?.EliteGroup,
|
||||
targetEvent?.HardLevelGroup,
|
||||
targetEvent?.Level,
|
||||
hardLevelConfig,
|
||||
eliteConfig
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={enemyIndex}
|
||||
className="group relative flex flex-col w-40 bg-base-100 rounded-2xl border border-base-300 shadow-md"
|
||||
>
|
||||
<div className="badge badge-warning badge-sm font-bold absolute top-2 right-2 z-10 shadow-sm">
|
||||
Lv. {targetEvent.Level}
|
||||
</div>
|
||||
|
||||
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
|
||||
{mapMonster?.[waveValue.toString()]?.Image?.IconPath && (
|
||||
<div className="relative w-16 h-16 rounded-full border-2 border-base-300 shadow-md overflow-hidden group-hover:scale-110 transition-transform duration-300 bg-base-100">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${mapMonster?.[waveValue.toString()]?.Image?.IconPath}`}
|
||||
alt="Enemy Icon"
|
||||
width={150}
|
||||
height={150}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col px-1 pb-2 pt-2">
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-error">HP</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.hp.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-info">Speed</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.spd.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-base-content/70">Toughness</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.stance.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 pt-2 border-t border-base-300 flex flex-col items-center">
|
||||
<span className="text-[10px] text-base-content/60 font-bold uppercase tracking-widest mb-1.5">
|
||||
Weakness
|
||||
</span>
|
||||
<div className="flex items-center justify-center gap-1.5 flex-wrap">
|
||||
{mapMonster?.[waveValue.toString()]?.StanceWeakList?.map((icon, iconIndex) => (
|
||||
<Image
|
||||
key={iconIndex}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${damageType[icon]?.Icon}`}
|
||||
alt={icon}
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-6 w-6 object-contain rounded-full bg-base-300 border border-base-content/10 p-0.5 shadow-sm hover:scale-110 transition-transform"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+307
-307
@@ -1,308 +1,308 @@
|
||||
"use client"
|
||||
import { useEffect, useMemo } from "react";
|
||||
import SelectCustomText from "../select/customSelectText";
|
||||
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";;
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { MonsterStore } from "@/types";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
|
||||
export default function PeakBar() {
|
||||
const { locale } = useLocaleStore()
|
||||
const {
|
||||
peak_config,
|
||||
setPeakConfig
|
||||
} = useUserDataStore()
|
||||
const { mapMonster, mapPeak, damageType, eliteConfig, hardLevelConfig } = useDetailDataStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
|
||||
const listFloor = useMemo(() => {
|
||||
const peak = mapPeak?.[peak_config?.event_id?.toString()]
|
||||
if (!peak) return []
|
||||
|
||||
return [...peak.PreLevel, peak.BossLevel].filter(it => it != null)
|
||||
}, [peak_config, mapPeak])
|
||||
const eventSelected = useMemo(() => {
|
||||
return mapPeak?.[peak_config?.event_id?.toString()]
|
||||
}, [peak_config, mapPeak])
|
||||
|
||||
const bossConfig = useMemo(() => {
|
||||
return mapPeak?.[peak_config?.event_id?.toString()]?.BossConfig;
|
||||
}, [peak_config, mapPeak])
|
||||
|
||||
const challengeSelected = useMemo(() => {
|
||||
const challenge = structuredClone(listFloor?.find((peak) => peak?.ID === peak_config.challenge_id))
|
||||
if (
|
||||
challenge
|
||||
&& challenge.ID === mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID
|
||||
&& bossConfig
|
||||
&& peak_config?.boss_mode === "Hard"
|
||||
) {
|
||||
return { challenge: bossConfig, isBoss: true }
|
||||
}
|
||||
return {
|
||||
challenge: challenge,
|
||||
isBoss: challenge?.ID === mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID,
|
||||
}
|
||||
}, [peak_config, listFloor, mapPeak, bossConfig])
|
||||
|
||||
useEffect(() => {
|
||||
if (!challengeSelected.challenge) return
|
||||
if (peak_config.event_id !== 0 && peak_config.challenge_id !== 0 && challengeSelected) {
|
||||
const newBattleConfig = structuredClone(peak_config)
|
||||
newBattleConfig.cycle_count = 6
|
||||
newBattleConfig.blessings = []
|
||||
for (const value of challengeSelected?.challenge?.MazeBuff) {
|
||||
newBattleConfig.blessings.push({
|
||||
id: value.ID,
|
||||
level: 1
|
||||
})
|
||||
}
|
||||
if (peak_config.buff_id !== 0 && challengeSelected.isBoss) {
|
||||
newBattleConfig.blessings.push({
|
||||
id: peak_config.buff_id,
|
||||
level: 1
|
||||
})
|
||||
}
|
||||
newBattleConfig.monsters = []
|
||||
newBattleConfig.stage_id = challengeSelected.challenge.EventList[0].ID
|
||||
for (const wave of challengeSelected.challenge.EventList[0].MonsterList) {
|
||||
if (!wave) continue
|
||||
const newWave: MonsterStore[] = []
|
||||
for (const value of Object.values(wave)) {
|
||||
if (!value) continue
|
||||
newWave.push({
|
||||
monster_id: value,
|
||||
level: challengeSelected.challenge.EventList[0].Level,
|
||||
amount: 1,
|
||||
})
|
||||
}
|
||||
newBattleConfig.monsters.push(newWave)
|
||||
}
|
||||
|
||||
setPeakConfig(newBattleConfig)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
peak_config.event_id,
|
||||
peak_config.challenge_id,
|
||||
peak_config.buff_id,
|
||||
peak_config.boss_mode,
|
||||
mapPeak,
|
||||
])
|
||||
|
||||
if (!mapPeak) return null
|
||||
|
||||
return (
|
||||
<div className="py-8 relative">
|
||||
|
||||
{/* Title Card */}
|
||||
<div className="rounded-xl p-4 mb-2 border border-warning">
|
||||
<div className="mb-4 w-full">
|
||||
<SelectCustomText
|
||||
customSet={Object.values(mapPeak).sort((a, b) => b.ID - a.ID).map((peak) => ({
|
||||
id: peak.ID.toString(),
|
||||
name: `${getLocaleName(locale, peak.Name)} (${peak.ID})`,
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={peak_config.event_id.toString()}
|
||||
placeholder={transI18n("selectPEAKEvent")}
|
||||
setSelectedCustomSet={(id) => setPeakConfig({ ...peak_config, event_id: Number(id), challenge_id: 0, buff_id: 0 })}
|
||||
/>
|
||||
</div>
|
||||
{/* Settings */}
|
||||
<div className={
|
||||
`grid grid-cols-1
|
||||
${eventSelected && eventSelected.BossLevel?.ID === peak_config.challenge_id ? "md:grid-cols-2" : ""}
|
||||
gap-4 mb-2 justify-items-center items-center w-full`}
|
||||
>
|
||||
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
|
||||
</label>
|
||||
<select
|
||||
value={peak_config.challenge_id}
|
||||
className="select select-success w-full"
|
||||
onChange={(e) => setPeakConfig({ ...peak_config, challenge_id: Number(e.target.value) })}
|
||||
>
|
||||
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
|
||||
{listFloor.map((peak) => (
|
||||
<option key={peak.ID} value={peak.ID}>{getLocaleName(locale, peak.Name)}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{eventSelected && eventSelected.BossLevel?.ID === peak_config.challenge_id && (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{transI18n("mode")}:{" "}</span>
|
||||
</label>
|
||||
<select
|
||||
value={peak_config.boss_mode}
|
||||
className="select select-success w-full"
|
||||
onChange={(e) => setPeakConfig({ ...peak_config, boss_mode: e.target.value })}
|
||||
>
|
||||
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
|
||||
<option value="Normal">{transI18n("normalMode")}</option>
|
||||
<option value="Hard">{transI18n("hardMode")}</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<div className="label-text font-bold text-success mb-2">StageId: {peak_config?.stage_id}</div>
|
||||
{
|
||||
eventSelected
|
||||
&& eventSelected.BossLevel?.ID === peak_config.challenge_id
|
||||
&& bossConfig
|
||||
&& (
|
||||
<div className="mb-4 w-full">
|
||||
<SelectCustomText
|
||||
customSet={
|
||||
bossConfig.BuffList.map((buff) => ({
|
||||
id: buff.ID.toString(),
|
||||
name: getLocaleName(locale, buff?.Name || ""),
|
||||
description: replaceByParam(getLocaleName(locale, buff?.Desc || ""), buff?.Param || []),
|
||||
}))
|
||||
}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={peak_config?.buff_id?.toString()}
|
||||
placeholder={transI18n("selectBuff")}
|
||||
setSelectedCustomSet={(id) => setPeakConfig({ ...peak_config, buff_id: Number(id) })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{/* Turbulence Buff */}
|
||||
|
||||
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
|
||||
<h2 className="text-2xl font-bold mb-2 text-info">
|
||||
{transI18n("turbulenceBuff")}
|
||||
</h2>
|
||||
|
||||
{challengeSelected?.challenge && challengeSelected?.challenge?.MazeBuff?.length > 0 ? (
|
||||
challengeSelected.challenge.MazeBuff.map((subOption, index) => (
|
||||
<div key={index}>
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">
|
||||
{index + 1}. {getLocaleName(locale, subOption.Name)}
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
className="text-base"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, subOption.Desc),
|
||||
subOption.Param || []
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enemy Waves */}
|
||||
|
||||
{(peak_config?.challenge_id ?? 0) !== 0 && (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
|
||||
<div className="rounded-xl p-4 mt-2 border border-warning">
|
||||
<h2 className="text-2xl font-bold mb-2 text-info">{getLocaleName(locale, challengeSelected?.challenge?.Name)}</h2>
|
||||
|
||||
|
||||
{challengeSelected?.challenge && Object.values(challengeSelected?.challenge?.EventList?.[0]?.Infinite || []).map((waveValue, waveIndex) => (
|
||||
<div key={waveIndex} className="mb-6">
|
||||
<h3 className="text-lg font-semibold">{transI18n("wave")} {waveIndex + 1}</h3>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{Array.from(new Set(waveValue.MonsterList)).map((monsterId, enemyIndex) => {
|
||||
const monsterStats = calcMonsterStats(
|
||||
mapMonster?.[monsterId.toString()],
|
||||
waveValue.EliteGroup,
|
||||
challengeSelected?.challenge?.EventList?.[0]?.HardLevelGroup || 0,
|
||||
challengeSelected?.challenge?.EventList?.[0]?.Level || 0,
|
||||
hardLevelConfig,
|
||||
eliteConfig
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={enemyIndex}
|
||||
className="group relative flex flex-col w-40 bg-base-100 rounded-2xl border border-base-300 shadow-md"
|
||||
>
|
||||
<div className="badge badge-warning badge-sm font-bold absolute top-2 right-2 z-10 shadow-sm">
|
||||
Lv. {challengeSelected?.challenge?.EventList?.[0]?.Level}
|
||||
</div>
|
||||
|
||||
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
|
||||
{mapMonster?.[monsterId.toString()]?.Image?.IconPath && (
|
||||
<div className="relative w-16 h-16 rounded-full border-2 border-base-300 shadow-md overflow-hidden group-hover:scale-110 transition-transform duration-300 bg-base-100">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${mapMonster?.[monsterId.toString()]?.Image?.IconPath}`}
|
||||
alt="Enemy Icon"
|
||||
width={150}
|
||||
height={150}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col px-1 pb-2 pt-2">
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-error">HP</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.hp.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-info">Speed</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.spd.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-base-content/70">Toughness</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.stance.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 pt-2 border-t border-base-300 flex flex-col items-center">
|
||||
<span className="text-[10px] text-base-content/60 font-bold uppercase tracking-widest mb-1.5">
|
||||
Weakness
|
||||
</span>
|
||||
<div className="flex items-center justify-center gap-1.5 flex-wrap">
|
||||
{mapMonster?.[monsterId.toString()]?.StanceWeakList?.map((icon, iconIndex) => (
|
||||
<Image
|
||||
key={iconIndex}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${damageType[icon]?.Icon}`}
|
||||
alt={icon}
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-6 w-6 object-contain rounded-full bg-base-300 border border-base-content/10 p-0.5 shadow-sm hover:scale-110 transition-transform"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
"use client"
|
||||
import { useEffect, useMemo } from "react";
|
||||
import SelectCustomText from "../select/customSelectText";
|
||||
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";;
|
||||
import Image from "next/image";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { MonsterStore } from "@/types";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
|
||||
export default function PeakBar() {
|
||||
const { locale } = useLocaleStore()
|
||||
const {
|
||||
peak_config,
|
||||
setPeakConfig
|
||||
} = useUserDataStore()
|
||||
const { mapMonster, mapPeak, damageType, eliteConfig, hardLevelConfig } = useDetailDataStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
|
||||
const listFloor = useMemo(() => {
|
||||
const peak = mapPeak?.[peak_config?.event_id?.toString()]
|
||||
if (!peak) return []
|
||||
|
||||
return [...peak.PreLevel, peak.BossLevel].filter(it => it != null)
|
||||
}, [peak_config, mapPeak])
|
||||
const eventSelected = useMemo(() => {
|
||||
return mapPeak?.[peak_config?.event_id?.toString()]
|
||||
}, [peak_config, mapPeak])
|
||||
|
||||
const bossConfig = useMemo(() => {
|
||||
return mapPeak?.[peak_config?.event_id?.toString()]?.BossConfig;
|
||||
}, [peak_config, mapPeak])
|
||||
|
||||
const challengeSelected = useMemo(() => {
|
||||
const challenge = structuredClone(listFloor?.find((peak) => peak?.ID === peak_config.challenge_id))
|
||||
if (
|
||||
challenge
|
||||
&& challenge.ID === mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID
|
||||
&& bossConfig
|
||||
&& peak_config?.boss_mode === "Hard"
|
||||
) {
|
||||
return { challenge: bossConfig, isBoss: true }
|
||||
}
|
||||
return {
|
||||
challenge: challenge,
|
||||
isBoss: challenge?.ID === mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID,
|
||||
}
|
||||
}, [peak_config, listFloor, mapPeak, bossConfig])
|
||||
|
||||
useEffect(() => {
|
||||
if (!challengeSelected.challenge) return
|
||||
if (peak_config.event_id !== 0 && peak_config.challenge_id !== 0 && challengeSelected) {
|
||||
const newBattleConfig = structuredClone(peak_config)
|
||||
newBattleConfig.cycle_count = 6
|
||||
newBattleConfig.blessings = []
|
||||
for (const value of challengeSelected?.challenge?.MazeBuff) {
|
||||
newBattleConfig.blessings.push({
|
||||
id: value.ID,
|
||||
level: 1
|
||||
})
|
||||
}
|
||||
if (peak_config.buff_id !== 0 && challengeSelected.isBoss) {
|
||||
newBattleConfig.blessings.push({
|
||||
id: peak_config.buff_id,
|
||||
level: 1
|
||||
})
|
||||
}
|
||||
newBattleConfig.monsters = []
|
||||
newBattleConfig.stage_id = challengeSelected.challenge.EventList[0].ID
|
||||
for (const wave of challengeSelected.challenge.EventList[0].MonsterList) {
|
||||
if (!wave) continue
|
||||
const newWave: MonsterStore[] = []
|
||||
for (const value of Object.values(wave)) {
|
||||
if (!value) continue
|
||||
newWave.push({
|
||||
monster_id: value,
|
||||
level: challengeSelected.challenge.EventList[0].Level,
|
||||
amount: 1,
|
||||
})
|
||||
}
|
||||
newBattleConfig.monsters.push(newWave)
|
||||
}
|
||||
|
||||
setPeakConfig(newBattleConfig)
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
peak_config.event_id,
|
||||
peak_config.challenge_id,
|
||||
peak_config.buff_id,
|
||||
peak_config.boss_mode,
|
||||
mapPeak,
|
||||
])
|
||||
|
||||
if (!mapPeak) return null
|
||||
|
||||
return (
|
||||
<div className="py-8 relative">
|
||||
|
||||
{/* Title Card */}
|
||||
<div className="rounded-xl p-4 mb-2 border border-warning">
|
||||
<div className="mb-4 w-full">
|
||||
<SelectCustomText
|
||||
customSet={Object.values(mapPeak).sort((a, b) => b.ID - a.ID).map((peak) => ({
|
||||
id: peak.ID.toString(),
|
||||
name: `${getLocaleName(locale, peak.Name)} (${peak.ID})`,
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={peak_config.event_id.toString()}
|
||||
placeholder={transI18n("selectPEAKEvent")}
|
||||
setSelectedCustomSet={(id) => setPeakConfig({ ...peak_config, event_id: Number(id), challenge_id: 0, buff_id: 0 })}
|
||||
/>
|
||||
</div>
|
||||
{/* Settings */}
|
||||
<div className={
|
||||
`grid grid-cols-1
|
||||
${eventSelected && eventSelected.BossLevel?.ID === peak_config.challenge_id ? "md:grid-cols-2" : ""}
|
||||
gap-4 mb-2 justify-items-center items-center w-full`}
|
||||
>
|
||||
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
|
||||
</label>
|
||||
<select
|
||||
value={peak_config.challenge_id}
|
||||
className="select select-success w-full"
|
||||
onChange={(e) => setPeakConfig({ ...peak_config, challenge_id: Number(e.target.value) })}
|
||||
>
|
||||
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
|
||||
{listFloor.map((peak) => (
|
||||
<option key={peak.ID} value={peak.ID}>{getLocaleName(locale, peak.Name)}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{eventSelected && eventSelected.BossLevel?.ID === peak_config.challenge_id && (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{transI18n("mode")}:{" "}</span>
|
||||
</label>
|
||||
<select
|
||||
value={peak_config.boss_mode}
|
||||
className="select select-success w-full"
|
||||
onChange={(e) => setPeakConfig({ ...peak_config, boss_mode: e.target.value })}
|
||||
>
|
||||
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
|
||||
<option value="Normal">{transI18n("normalMode")}</option>
|
||||
<option value="Hard">{transI18n("hardMode")}</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
<div className="label-text font-bold text-success mb-2">StageId: {peak_config?.stage_id}</div>
|
||||
{
|
||||
eventSelected
|
||||
&& eventSelected.BossLevel?.ID === peak_config.challenge_id
|
||||
&& bossConfig
|
||||
&& (
|
||||
<div className="mb-4 w-full">
|
||||
<SelectCustomText
|
||||
customSet={
|
||||
bossConfig.BuffList.map((buff) => ({
|
||||
id: buff.ID.toString(),
|
||||
name: getLocaleName(locale, buff?.Name || ""),
|
||||
description: replaceByParam(getLocaleName(locale, buff?.Desc || ""), buff?.Param || []),
|
||||
}))
|
||||
}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={peak_config?.buff_id?.toString()}
|
||||
placeholder={transI18n("selectBuff")}
|
||||
setSelectedCustomSet={(id) => setPeakConfig({ ...peak_config, buff_id: Number(id) })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
{/* Turbulence Buff */}
|
||||
|
||||
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
|
||||
<h2 className="text-2xl font-bold mb-2 text-info">
|
||||
{transI18n("turbulenceBuff")}
|
||||
</h2>
|
||||
|
||||
{challengeSelected?.challenge && challengeSelected?.challenge?.MazeBuff?.length > 0 ? (
|
||||
challengeSelected.challenge.MazeBuff.map((subOption, index) => (
|
||||
<div key={index}>
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">
|
||||
{index + 1}. {getLocaleName(locale, subOption.Name)}
|
||||
</span>
|
||||
</label>
|
||||
<div
|
||||
className="text-base"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, subOption.Desc),
|
||||
subOption.Param || []
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enemy Waves */}
|
||||
|
||||
{(peak_config?.challenge_id ?? 0) !== 0 && (
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
|
||||
<div className="rounded-xl p-4 mt-2 border border-warning">
|
||||
<h2 className="text-2xl font-bold mb-2 text-info">{getLocaleName(locale, challengeSelected?.challenge?.Name)}</h2>
|
||||
|
||||
|
||||
{challengeSelected?.challenge && Object.values(challengeSelected?.challenge?.EventList?.[0]?.Infinite || []).map((waveValue, waveIndex) => (
|
||||
<div key={waveIndex} className="mb-6">
|
||||
<h3 className="text-lg font-semibold">{transI18n("wave")} {waveIndex + 1}</h3>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{Array.from(new Set(waveValue.MonsterList)).map((monsterId, enemyIndex) => {
|
||||
const monsterStats = calcMonsterStats(
|
||||
mapMonster?.[monsterId.toString()],
|
||||
waveValue.EliteGroup,
|
||||
challengeSelected?.challenge?.EventList?.[0]?.HardLevelGroup || 0,
|
||||
challengeSelected?.challenge?.EventList?.[0]?.Level || 0,
|
||||
hardLevelConfig,
|
||||
eliteConfig
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={enemyIndex}
|
||||
className="group relative flex flex-col w-40 bg-base-100 rounded-2xl border border-base-300 shadow-md"
|
||||
>
|
||||
<div className="badge badge-warning badge-sm font-bold absolute top-2 right-2 z-10 shadow-sm">
|
||||
Lv. {challengeSelected?.challenge?.EventList?.[0]?.Level}
|
||||
</div>
|
||||
|
||||
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
|
||||
{mapMonster?.[monsterId.toString()]?.Image?.IconPath && (
|
||||
<div className="relative w-16 h-16 rounded-full border-2 border-base-300 shadow-md overflow-hidden group-hover:scale-110 transition-transform duration-300 bg-base-100">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${mapMonster?.[monsterId.toString()]?.Image?.IconPath}`}
|
||||
alt="Enemy Icon"
|
||||
width={150}
|
||||
height={150}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col px-1 pb-2 pt-2">
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-error">HP</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.hp.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-info">Speed</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.spd.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-base-content/70">Toughness</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.stance.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 pt-2 border-t border-base-300 flex flex-col items-center">
|
||||
<span className="text-[10px] text-base-content/60 font-bold uppercase tracking-widest mb-1.5">
|
||||
Weakness
|
||||
</span>
|
||||
<div className="flex items-center justify-center gap-1.5 flex-wrap">
|
||||
{mapMonster?.[monsterId.toString()]?.StanceWeakList?.map((icon, iconIndex) => (
|
||||
<Image
|
||||
key={iconIndex}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${damageType[icon]?.Icon}`}
|
||||
alt={icon}
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-6 w-6 object-contain rounded-full bg-base-300 border border-base-content/10 p-0.5 shadow-sm hover:scale-110 transition-transform"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+340
-340
@@ -1,341 +1,341 @@
|
||||
"use client"
|
||||
import { useEffect, useMemo } from "react";
|
||||
import SelectCustomText from "../select/customSelectText";
|
||||
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import Image from "next/image";
|
||||
import { MonsterStore, PFEvent } from "@/types";
|
||||
import { useTranslations } from "next-intl";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
|
||||
export default function PfBar() {
|
||||
const { locale } = useLocaleStore()
|
||||
const {
|
||||
pf_config,
|
||||
setPfConfig
|
||||
} = useUserDataStore()
|
||||
const { mapMonster, mapPF, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
|
||||
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const challengeSelected = useMemo(() => {
|
||||
return mapPF[pf_config.event_id.toString()]?.Level.find((pf) => pf.ID === pf_config.challenge_id)
|
||||
}, [pf_config, mapPF])
|
||||
|
||||
const eventSelected = useMemo(() => {
|
||||
return mapPF[pf_config.event_id.toString()]
|
||||
}, [pf_config, mapPF])
|
||||
|
||||
const floorSideList = useMemo(() => {
|
||||
if (!eventSelected) return [];
|
||||
|
||||
const floorList = [
|
||||
{
|
||||
id: "firstNode",
|
||||
name: transI18n("firstNode"),
|
||||
wave: transI18n("firstNodeEnemies")
|
||||
},
|
||||
{
|
||||
id: "secondNode",
|
||||
name: transI18n("secondNode"),
|
||||
wave: transI18n("secondNodeEnemies")
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
if (eventSelected?.Tierce && eventSelected.Tierce.PreChallenge === pf_config.challenge_id) {
|
||||
floorList.push({
|
||||
id: "thirdNode",
|
||||
name: transI18n("thirdNode"),
|
||||
wave: transI18n("thirdNodeEnemies")
|
||||
})
|
||||
}
|
||||
return floorList
|
||||
}, [pf_config.challenge_id, eventSelected, transI18n])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!challengeSelected || pf_config.event_id === 0 || pf_config.challenge_id === 0) {
|
||||
return
|
||||
}
|
||||
const newBattleConfig = structuredClone(pf_config)
|
||||
newBattleConfig.cycle_count = challengeSelected.TurnLimit
|
||||
newBattleConfig.blessings = []
|
||||
if (pf_config.buff_id !== 0) {
|
||||
newBattleConfig.blessings.push({
|
||||
id: pf_config.buff_id,
|
||||
level: 1
|
||||
})
|
||||
}
|
||||
if (challengeSelected) {
|
||||
challengeSelected.MazeBuff.map((item) => {
|
||||
newBattleConfig.blessings.push({
|
||||
id: item.ID,
|
||||
level: 1
|
||||
})
|
||||
})
|
||||
}
|
||||
newBattleConfig.monsters = []
|
||||
newBattleConfig.stage_id = 0
|
||||
|
||||
let targetEventList: PFEvent[] = []
|
||||
if (pf_config.floor_side === "firstNode" && challengeSelected.EventList1.length > 0) {
|
||||
targetEventList = challengeSelected.EventList1
|
||||
} else if (pf_config.floor_side === "secondNode" && challengeSelected.EventList2.length > 0) {
|
||||
targetEventList = challengeSelected.EventList2
|
||||
} else if (pf_config.floor_side === "thirdNode" && eventSelected?.Tierce && eventSelected.Tierce.EventList.length > 0) {
|
||||
targetEventList = eventSelected.Tierce.EventList
|
||||
}
|
||||
|
||||
if (targetEventList.length > 0) {
|
||||
newBattleConfig.stage_id = targetEventList[0].ID
|
||||
for (const wave of targetEventList[0].MonsterList) {
|
||||
const newWave: MonsterStore[] = []
|
||||
for (const value of Object.values(wave)) {
|
||||
newWave.push({
|
||||
monster_id: value as number,
|
||||
level: targetEventList[0].Level,
|
||||
amount: 1,
|
||||
})
|
||||
}
|
||||
newBattleConfig.monsters.push(newWave)
|
||||
}
|
||||
}
|
||||
|
||||
setPfConfig(newBattleConfig)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
challengeSelected,
|
||||
eventSelected,
|
||||
pf_config.event_id,
|
||||
pf_config.challenge_id,
|
||||
pf_config.floor_side,
|
||||
pf_config.buff_id,
|
||||
mapPF,
|
||||
])
|
||||
if (!mapPF) return null
|
||||
|
||||
return (
|
||||
<div className="py-8 relative">
|
||||
|
||||
{/* Title Card */}
|
||||
<div className="rounded-xl p-4 mb-2 border border-warning">
|
||||
<div className="mb-4 w-full">
|
||||
<SelectCustomText
|
||||
customSet={Object.values(mapPF).sort((a, b) => b.ID - a.ID).map((pf) => ({
|
||||
id: pf.ID.toString(),
|
||||
name: getLocaleName(locale, pf.Name),
|
||||
time: `${pf.BeginTime} - ${pf.EndTime}`,
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={pf_config.event_id.toString()}
|
||||
placeholder={transI18n("selectPFEvent")}
|
||||
setSelectedCustomSet={(id) => setPfConfig({
|
||||
...pf_config,
|
||||
event_id: Number(id),
|
||||
challenge_id: mapPF[Number(id)]?.Level.slice(-1)[0]?.ID || 0,
|
||||
buff_id: 0
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{/* Settings */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-2">
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
|
||||
</label>
|
||||
<select
|
||||
value={pf_config.challenge_id}
|
||||
className="select select-success"
|
||||
onChange={(e) => setPfConfig({ ...pf_config, challenge_id: Number(e.target.value) })}
|
||||
>
|
||||
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
|
||||
{eventSelected?.Level.map((pf) => (
|
||||
<option key={pf.ID} value={pf.ID}>{getLocaleName(locale, pf.Name)}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
|
||||
</label>
|
||||
<select
|
||||
value={pf_config.floor_side}
|
||||
className="select select-success"
|
||||
onChange={(e) => setPfConfig({ ...pf_config, floor_side: e.target.value })}
|
||||
>
|
||||
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
|
||||
{floorSideList.map((side) => (
|
||||
<option key={side.id} value={side.id}>{side.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="label-text font-bold text-success mb-2">StageId: {pf_config?.stage_id}</div>
|
||||
{eventSelected && (
|
||||
<div className="mb-4 w-full">
|
||||
<SelectCustomText
|
||||
customSet={eventSelected.Option.map((buff) => ({
|
||||
id: buff.ID?.toString() || "",
|
||||
name: getLocaleName(locale, buff?.Name) || "",
|
||||
description: replaceByParam(getLocaleName(locale, buff?.Desc) || "", buff?.Param || []),
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={pf_config?.buff_id?.toString()}
|
||||
placeholder={transI18n("selectBuff")}
|
||||
setSelectedCustomSet={(id) => setPfConfig({ ...pf_config, buff_id: Number(id) })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* Turbulence Buff */}
|
||||
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
|
||||
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2>
|
||||
{eventSelected && eventSelected.SubOption.length > 0 ? (
|
||||
eventSelected.SubOption.map((subOption, index) => (
|
||||
<div key={index}>
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{index + 1}. {getLocaleName(locale, subOption.Name)}</span>
|
||||
</label>
|
||||
<div
|
||||
className="text-base"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, subOption.Desc) || "",
|
||||
subOption.Param || []
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : eventSelected && challengeSelected && eventSelected.SubOption.length === 0 ? (
|
||||
challengeSelected?.MazeBuff?.map((buff, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-base"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, buff?.Desc) || "",
|
||||
buff?.Param || []
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enemy Waves */}
|
||||
|
||||
{(pf_config?.challenge_id ?? 0) !== 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{floorSideList.map((side, i) => {
|
||||
const eventList = side.id === "firstNode"
|
||||
? challengeSelected?.EventList1
|
||||
: side.id === "secondNode"
|
||||
? challengeSelected?.EventList2
|
||||
: side.id === "thirdNode"
|
||||
? eventSelected?.Tierce?.EventList
|
||||
: [];
|
||||
|
||||
if (!eventList || eventList.length === 0) return null;
|
||||
const targetEvent = eventList[0];
|
||||
|
||||
return (
|
||||
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
|
||||
<h2 className="text-2xl font-bold mb-2 text-info">{side.wave}</h2>
|
||||
|
||||
{targetEvent && Object.values(targetEvent.Infinite || []).map((waveValue, waveIndex) => (
|
||||
<div key={waveIndex} className="mb-6">
|
||||
<h3 className="text-lg font-semibold">{transI18n("wave")} {waveIndex + 1}</h3>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{Array.from(new Set(waveValue.MonsterList)).map((monsterId, enemyIndex) => {
|
||||
const monsterStats = calcMonsterStats(
|
||||
mapMonster?.[monsterId.toString()],
|
||||
waveValue.EliteGroup,
|
||||
targetEvent?.HardLevelGroup,
|
||||
targetEvent?.Level,
|
||||
hardLevelConfig,
|
||||
eliteConfig
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={enemyIndex}
|
||||
className="group relative flex flex-col w-40 bg-base-100 rounded-2xl border border-base-300 shadow-md"
|
||||
>
|
||||
<div className="badge badge-warning badge-sm font-bold absolute top-2 right-2 z-10 shadow-sm">
|
||||
Lv. {targetEvent.Level}
|
||||
</div>
|
||||
|
||||
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
|
||||
{mapMonster?.[monsterId.toString()]?.Image?.IconPath && (
|
||||
<div className="relative w-16 h-16 rounded-full border-2 border-base-300 shadow-md overflow-hidden group-hover:scale-110 transition-transform duration-300 bg-base-100">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${mapMonster?.[monsterId.toString()]?.Image?.IconPath}`}
|
||||
alt="Enemy Icon"
|
||||
width={150}
|
||||
height={150}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col px-1 pb-2 pt-2">
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-error">HP</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.hp.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-info">Speed</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.spd.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-base-content/70">Toughness</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.stance.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 pt-2 border-t border-base-300 flex flex-col items-center">
|
||||
<span className="text-[10px] text-base-content/60 font-bold uppercase tracking-widest mb-1.5">
|
||||
Weakness
|
||||
</span>
|
||||
<div className="flex items-center justify-center gap-1.5 flex-wrap">
|
||||
{mapMonster?.[monsterId.toString()]?.StanceWeakList?.map((icon, iconIndex) => (
|
||||
<Image
|
||||
key={iconIndex}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${damageType[icon]?.Icon}`}
|
||||
alt={icon}
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-6 w-6 object-contain rounded-full bg-base-300 border border-base-content/10 p-0.5 shadow-sm hover:scale-110 transition-transform"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
"use client"
|
||||
import { useEffect, useMemo } from "react";
|
||||
import SelectCustomText from "../select/customSelectText";
|
||||
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import Image from "next/image";
|
||||
import { MonsterStore, PFEvent } from "@/types";
|
||||
import { useTranslations } from "next-intl";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
|
||||
export default function PfBar() {
|
||||
const { locale } = useLocaleStore()
|
||||
const {
|
||||
pf_config,
|
||||
setPfConfig
|
||||
} = useUserDataStore()
|
||||
const { mapMonster, mapPF, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
|
||||
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const challengeSelected = useMemo(() => {
|
||||
return mapPF[pf_config.event_id.toString()]?.Level.find((pf) => pf.ID === pf_config.challenge_id)
|
||||
}, [pf_config, mapPF])
|
||||
|
||||
const eventSelected = useMemo(() => {
|
||||
return mapPF[pf_config.event_id.toString()]
|
||||
}, [pf_config, mapPF])
|
||||
|
||||
const floorSideList = useMemo(() => {
|
||||
if (!eventSelected) return [];
|
||||
|
||||
const floorList = [
|
||||
{
|
||||
id: "firstNode",
|
||||
name: transI18n("firstNode"),
|
||||
wave: transI18n("firstNodeEnemies")
|
||||
},
|
||||
{
|
||||
id: "secondNode",
|
||||
name: transI18n("secondNode"),
|
||||
wave: transI18n("secondNodeEnemies")
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
if (eventSelected?.Tierce && eventSelected.Tierce.PreChallenge === pf_config.challenge_id) {
|
||||
floorList.push({
|
||||
id: "thirdNode",
|
||||
name: transI18n("thirdNode"),
|
||||
wave: transI18n("thirdNodeEnemies")
|
||||
})
|
||||
}
|
||||
return floorList
|
||||
}, [pf_config.challenge_id, eventSelected, transI18n])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (!challengeSelected || pf_config.event_id === 0 || pf_config.challenge_id === 0) {
|
||||
return
|
||||
}
|
||||
const newBattleConfig = structuredClone(pf_config)
|
||||
newBattleConfig.cycle_count = challengeSelected.TurnLimit
|
||||
newBattleConfig.blessings = []
|
||||
if (pf_config.buff_id !== 0) {
|
||||
newBattleConfig.blessings.push({
|
||||
id: pf_config.buff_id,
|
||||
level: 1
|
||||
})
|
||||
}
|
||||
if (challengeSelected) {
|
||||
challengeSelected.MazeBuff.map((item) => {
|
||||
newBattleConfig.blessings.push({
|
||||
id: item.ID,
|
||||
level: 1
|
||||
})
|
||||
})
|
||||
}
|
||||
newBattleConfig.monsters = []
|
||||
newBattleConfig.stage_id = 0
|
||||
|
||||
let targetEventList: PFEvent[] = []
|
||||
if (pf_config.floor_side === "firstNode" && challengeSelected.EventList1.length > 0) {
|
||||
targetEventList = challengeSelected.EventList1
|
||||
} else if (pf_config.floor_side === "secondNode" && challengeSelected.EventList2.length > 0) {
|
||||
targetEventList = challengeSelected.EventList2
|
||||
} else if (pf_config.floor_side === "thirdNode" && eventSelected?.Tierce && eventSelected.Tierce.EventList.length > 0) {
|
||||
targetEventList = eventSelected.Tierce.EventList
|
||||
}
|
||||
|
||||
if (targetEventList.length > 0) {
|
||||
newBattleConfig.stage_id = targetEventList[0].ID
|
||||
for (const wave of targetEventList[0].MonsterList) {
|
||||
const newWave: MonsterStore[] = []
|
||||
for (const value of Object.values(wave)) {
|
||||
newWave.push({
|
||||
monster_id: value as number,
|
||||
level: targetEventList[0].Level,
|
||||
amount: 1,
|
||||
})
|
||||
}
|
||||
newBattleConfig.monsters.push(newWave)
|
||||
}
|
||||
}
|
||||
|
||||
setPfConfig(newBattleConfig)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
challengeSelected,
|
||||
eventSelected,
|
||||
pf_config.event_id,
|
||||
pf_config.challenge_id,
|
||||
pf_config.floor_side,
|
||||
pf_config.buff_id,
|
||||
mapPF,
|
||||
])
|
||||
if (!mapPF) return null
|
||||
|
||||
return (
|
||||
<div className="py-8 relative">
|
||||
|
||||
{/* Title Card */}
|
||||
<div className="rounded-xl p-4 mb-2 border border-warning">
|
||||
<div className="mb-4 w-full">
|
||||
<SelectCustomText
|
||||
customSet={Object.values(mapPF).sort((a, b) => b.ID - a.ID).map((pf) => ({
|
||||
id: pf.ID.toString(),
|
||||
name: getLocaleName(locale, pf.Name),
|
||||
time: `${pf.BeginTime} - ${pf.EndTime}`,
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={pf_config.event_id.toString()}
|
||||
placeholder={transI18n("selectPFEvent")}
|
||||
setSelectedCustomSet={(id) => setPfConfig({
|
||||
...pf_config,
|
||||
event_id: Number(id),
|
||||
challenge_id: mapPF[Number(id)]?.Level.slice(-1)[0]?.ID || 0,
|
||||
buff_id: 0
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
{/* Settings */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-2">
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
|
||||
</label>
|
||||
<select
|
||||
value={pf_config.challenge_id}
|
||||
className="select select-success"
|
||||
onChange={(e) => setPfConfig({ ...pf_config, challenge_id: Number(e.target.value) })}
|
||||
>
|
||||
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
|
||||
{eventSelected?.Level.map((pf) => (
|
||||
<option key={pf.ID} value={pf.ID}>{getLocaleName(locale, pf.Name)}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{transI18n("side")}:{" "}</span>
|
||||
</label>
|
||||
<select
|
||||
value={pf_config.floor_side}
|
||||
className="select select-success"
|
||||
onChange={(e) => setPfConfig({ ...pf_config, floor_side: e.target.value })}
|
||||
>
|
||||
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
|
||||
{floorSideList.map((side) => (
|
||||
<option key={side.id} value={side.id}>{side.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="label-text font-bold text-success mb-2">StageId: {pf_config?.stage_id}</div>
|
||||
{eventSelected && (
|
||||
<div className="mb-4 w-full">
|
||||
<SelectCustomText
|
||||
customSet={eventSelected.Option.map((buff) => ({
|
||||
id: buff.ID?.toString() || "",
|
||||
name: getLocaleName(locale, buff?.Name) || "",
|
||||
description: replaceByParam(getLocaleName(locale, buff?.Desc) || "", buff?.Param || []),
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={pf_config?.buff_id?.toString()}
|
||||
placeholder={transI18n("selectBuff")}
|
||||
setSelectedCustomSet={(id) => setPfConfig({ ...pf_config, buff_id: Number(id) })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* Turbulence Buff */}
|
||||
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
|
||||
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2>
|
||||
{eventSelected && eventSelected.SubOption.length > 0 ? (
|
||||
eventSelected.SubOption.map((subOption, index) => (
|
||||
<div key={index}>
|
||||
<label className="label">
|
||||
<span className="label-text font-bold text-success">{index + 1}. {getLocaleName(locale, subOption.Name)}</span>
|
||||
</label>
|
||||
<div
|
||||
className="text-base"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, subOption.Desc) || "",
|
||||
subOption.Param || []
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
) : eventSelected && challengeSelected && eventSelected.SubOption.length === 0 ? (
|
||||
challengeSelected?.MazeBuff?.map((buff, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="text-base"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, buff?.Desc) || "",
|
||||
buff?.Param || []
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enemy Waves */}
|
||||
|
||||
{(pf_config?.challenge_id ?? 0) !== 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{floorSideList.map((side, i) => {
|
||||
const eventList = side.id === "firstNode"
|
||||
? challengeSelected?.EventList1
|
||||
: side.id === "secondNode"
|
||||
? challengeSelected?.EventList2
|
||||
: side.id === "thirdNode"
|
||||
? eventSelected?.Tierce?.EventList
|
||||
: [];
|
||||
|
||||
if (!eventList || eventList.length === 0) return null;
|
||||
const targetEvent = eventList[0];
|
||||
|
||||
return (
|
||||
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
|
||||
<h2 className="text-2xl font-bold mb-2 text-info">{side.wave}</h2>
|
||||
|
||||
{targetEvent && Object.values(targetEvent.Infinite || []).map((waveValue, waveIndex) => (
|
||||
<div key={waveIndex} className="mb-6">
|
||||
<h3 className="text-lg font-semibold">{transI18n("wave")} {waveIndex + 1}</h3>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{Array.from(new Set(waveValue.MonsterList)).map((monsterId, enemyIndex) => {
|
||||
const monsterStats = calcMonsterStats(
|
||||
mapMonster?.[monsterId.toString()],
|
||||
waveValue.EliteGroup,
|
||||
targetEvent?.HardLevelGroup,
|
||||
targetEvent?.Level,
|
||||
hardLevelConfig,
|
||||
eliteConfig
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={enemyIndex}
|
||||
className="group relative flex flex-col w-40 bg-base-100 rounded-2xl border border-base-300 shadow-md"
|
||||
>
|
||||
<div className="badge badge-warning badge-sm font-bold absolute top-2 right-2 z-10 shadow-sm">
|
||||
Lv. {targetEvent.Level}
|
||||
</div>
|
||||
|
||||
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
|
||||
{mapMonster?.[monsterId.toString()]?.Image?.IconPath && (
|
||||
<div className="relative w-16 h-16 rounded-full border-2 border-base-300 shadow-md overflow-hidden group-hover:scale-110 transition-transform duration-300 bg-base-100">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${mapMonster?.[monsterId.toString()]?.Image?.IconPath}`}
|
||||
alt="Enemy Icon"
|
||||
width={150}
|
||||
height={150}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col px-1 pb-2 pt-2">
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-error">HP</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.hp.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-info">Speed</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.spd.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||
<span className="text-xs font-semibold text-base-content/70">Toughness</span>
|
||||
<span className="text-sm font-bold text-base-content">{monsterStats.stance.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 pt-2 border-t border-base-300 flex flex-col items-center">
|
||||
<span className="text-[10px] text-base-content/60 font-bold uppercase tracking-widest mb-1.5">
|
||||
Weakness
|
||||
</span>
|
||||
<div className="flex items-center justify-center gap-1.5 flex-wrap">
|
||||
{mapMonster?.[monsterId.toString()]?.StanceWeakList?.map((icon, iconIndex) => (
|
||||
<Image
|
||||
key={iconIndex}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${damageType[icon]?.Icon}`}
|
||||
alt={icon}
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-6 w-6 object-contain rounded-full bg-base-300 border border-base-content/10 p-0.5 shadow-sm hover:scale-110 transition-transform"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
|
||||
'use client'
|
||||
import { parseRuby } from "@/helper";
|
||||
|
||||
interface TextProps {
|
||||
text: string;
|
||||
locale: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ParseText({ text, locale, className }: TextProps) {
|
||||
if (locale === "ja") {
|
||||
return <div className={className} dangerouslySetInnerHTML={{ __html: parseRuby(text) }} />;
|
||||
}
|
||||
return <div className={className}>{text}</div>;
|
||||
|
||||
'use client'
|
||||
import { parseRuby } from "@/helper";
|
||||
|
||||
interface TextProps {
|
||||
text: string;
|
||||
locale: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ParseText({ text, locale, className }: TextProps) {
|
||||
if (locale === "ja") {
|
||||
return <div className={className} dangerouslySetInnerHTML={{ __html: parseRuby(text) }} />;
|
||||
}
|
||||
return <div className={className}>{text}</div>;
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import React from 'react';
|
||||
|
||||
export default function QueryProviderWrapper({ children }: { children: React.ReactNode }) {
|
||||
const [queryClient] = React.useState(() => new QueryClient());
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import React from 'react';
|
||||
|
||||
export default function QueryProviderWrapper({ children }: { children: React.ReactNode }) {
|
||||
const [queryClient] = React.useState(() => new QueryClient());
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
+472
-472
@@ -1,473 +1,473 @@
|
||||
"use client"
|
||||
import NextImage from "next/image"
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useMemo } from "react";
|
||||
import { calcAffixBonus, calcBaseStatRaw, calcBonusStatRaw, calcMainAffixBonus, calcMainAffixBonusRaw, calcPromotion, calcSubAffixBonusRaw, getLocaleName, replaceByParam } from "@/helper";
|
||||
import { mappingStats } from "@/constant/constant";
|
||||
import RelicShowcase from "../showcaseCard/relicShowcase";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||
|
||||
export default function QuickView() {
|
||||
const { avatars } = useUserDataStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { locale } = useLocaleStore()
|
||||
const { avatarSelected, } = useCurrentDataStore()
|
||||
const { mainAffix, subAffix, mapRelicSet, mapLightCone, mapAvatar } = useDetailDataStore()
|
||||
|
||||
|
||||
const avatarSkillTree = useMemo(() => {
|
||||
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
|
||||
if (avatars[avatarSelected?.ID?.toString()].enhanced) {
|
||||
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
|
||||
}
|
||||
return avatarSelected?.SkillTrees || {}
|
||||
}, [avatarSelected, avatars])
|
||||
|
||||
const avatarData = useMemo(() => {
|
||||
if (!avatarSelected) return
|
||||
return avatars[avatarSelected?.ID?.toString()]
|
||||
}, [avatarSelected, avatars])
|
||||
|
||||
const avatarProfile = useMemo(() => {
|
||||
if (!avatarSelected || !avatarData) return
|
||||
return avatarData?.profileList?.[avatarData?.profileSelect]
|
||||
}, [avatarSelected, avatarData])
|
||||
|
||||
const relicEffects = useMemo(() => {
|
||||
const avatar = avatars[avatarSelected?.ID?.toString() || ""];
|
||||
const relicCount: { [key: string]: number } = {};
|
||||
if (avatar) {
|
||||
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
|
||||
if (relicCount[relic.relic_set_id]) {
|
||||
relicCount[relic.relic_set_id]++;
|
||||
} else {
|
||||
relicCount[relic.relic_set_id] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
const listEffects: { key: string, count: number }[] = [];
|
||||
Object.entries(relicCount).forEach(([key, value]) => {
|
||||
if (value >= 2) {
|
||||
listEffects.push({ key: key, count: value });
|
||||
}
|
||||
});
|
||||
return listEffects;
|
||||
}, [avatars, avatarSelected]);
|
||||
|
||||
const relicStats = useMemo(() => {
|
||||
if (!avatarSelected || !avatarProfile?.relics || !mainAffix || !subAffix) return
|
||||
|
||||
return Object.entries(avatarProfile?.relics).map(([key, value]) => {
|
||||
const mainAffixMap = mainAffix["5" + key]
|
||||
const subAffixMap = subAffix["5"]
|
||||
if (!mainAffixMap || !subAffixMap) return
|
||||
return {
|
||||
img: `${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${value.relic_set_id}_${key}.png`,
|
||||
mainAffix: {
|
||||
property: mainAffixMap?.[value?.main_affix_id]?.Property,
|
||||
level: value?.level,
|
||||
valueAffix: calcMainAffixBonus(mainAffixMap?.[value?.main_affix_id], value?.level),
|
||||
detail: mappingStats?.[mainAffixMap?.[value?.main_affix_id]?.Property]
|
||||
},
|
||||
subAffix: value?.sub_affixes?.map((subValue) => {
|
||||
return {
|
||||
property: subAffixMap?.[subValue?.sub_affix_id]?.Property,
|
||||
valueAffix: calcAffixBonus(subAffixMap?.[subValue?.sub_affix_id], subValue?.step, subValue?.count),
|
||||
detail: mappingStats?.[subAffixMap?.[subValue?.sub_affix_id]?.Property],
|
||||
step: subValue?.step,
|
||||
count: subValue?.count
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}, [avatarSelected, avatarProfile, mainAffix, subAffix])
|
||||
|
||||
const characterStats = useMemo(() => {
|
||||
if (!avatarSelected || !avatarData) return
|
||||
const charPromotion = calcPromotion(avatarData.level)
|
||||
|
||||
const statsData: Record<string, {
|
||||
value: number,
|
||||
base: number,
|
||||
name: string,
|
||||
icon: string,
|
||||
unit: string,
|
||||
round: number
|
||||
}> = {
|
||||
HP: {
|
||||
value: calcBaseStatRaw(
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
|
||||
avatarData.level
|
||||
),
|
||||
base: calcBaseStatRaw(
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
|
||||
avatarData.level
|
||||
),
|
||||
name: "HP",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconMaxHP.png",
|
||||
unit: "",
|
||||
round: 0
|
||||
},
|
||||
ATK: {
|
||||
value: calcBaseStatRaw(
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
|
||||
avatarData.level
|
||||
),
|
||||
base: calcBaseStatRaw(
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
|
||||
avatarData.level
|
||||
),
|
||||
name: "ATK",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconAttack.png",
|
||||
unit: "",
|
||||
round: 0
|
||||
},
|
||||
DEF: {
|
||||
value: calcBaseStatRaw(
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
|
||||
avatarData.level
|
||||
),
|
||||
base: calcBaseStatRaw(
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
|
||||
avatarData.level
|
||||
),
|
||||
name: "DEF",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconDefence.png",
|
||||
unit: "",
|
||||
round: 0
|
||||
},
|
||||
SPD: {
|
||||
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
|
||||
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
|
||||
name: "SPD",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconSpeed.png",
|
||||
unit: "",
|
||||
round: 1
|
||||
},
|
||||
CRITRate: {
|
||||
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
|
||||
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
|
||||
name: "CRIT Rate",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconCriticalChance.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
CRITDmg: {
|
||||
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
|
||||
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
|
||||
name: "CRIT DMG",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
BreakEffect: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Break Effect",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconBreakUp.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
EffectRES: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Effect RES",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconStatusResistance.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
EnergyRate: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Energy Rate",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
EffectHitRate: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Effect Hit Rate",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconStatusProbability.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
HealBoost: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Healing Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconHealRatio.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
PhysicalAdd: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Physical Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
FireAdd: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Fire Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
IceAdd: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Ice Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
ThunderAdd: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Thunder Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
WindAdd: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Wind Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
QuantumAdd: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Quantum Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
ImaginaryAdd: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Imaginary Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
ElationAdd: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Elation Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconJoy.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
}
|
||||
}
|
||||
|
||||
if (avatarProfile?.lightcone && mapLightCone[avatarProfile?.lightcone?.item_id]) {
|
||||
const lightconePromotion = calcPromotion(avatarProfile?.lightcone?.level)
|
||||
statsData.HP.value += calcBaseStatRaw(
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd,
|
||||
avatarProfile?.lightcone?.level
|
||||
)
|
||||
statsData.HP.base += calcBaseStatRaw(
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHPAdd,
|
||||
avatarProfile?.lightcone?.level
|
||||
)
|
||||
statsData.ATK.value += calcBaseStatRaw(
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
|
||||
avatarProfile?.lightcone?.level
|
||||
)
|
||||
statsData.ATK.base += calcBaseStatRaw(
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
|
||||
avatarProfile?.lightcone?.level
|
||||
)
|
||||
statsData.DEF.value += calcBaseStatRaw(
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
|
||||
avatarProfile?.lightcone?.level
|
||||
)
|
||||
statsData.DEF.base += calcBaseStatRaw(
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
|
||||
avatarProfile?.lightcone?.level
|
||||
)
|
||||
|
||||
const bonusData = mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Skills?.Level?.[avatarProfile?.lightcone.rank]?.Bonus
|
||||
if (bonusData && bonusData.length > 0) {
|
||||
const bonusSpd = bonusData.filter((bonus) => bonus.PropertyType === "BaseSpeed")
|
||||
const bonusOther = bonusData.filter((bonus) => bonus.PropertyType !== "BaseSpeed")
|
||||
bonusSpd.forEach((bonus) => {
|
||||
statsData.SPD.value += bonus.Value
|
||||
statsData.SPD.base += bonus.Value
|
||||
})
|
||||
bonusOther.forEach((bonus) => {
|
||||
const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
|
||||
if (statsBase && statsData[statsBase]) {
|
||||
statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
if (avatarSkillTree) {
|
||||
Object.values(avatarSkillTree).forEach((value) => {
|
||||
if (value?.["1"]
|
||||
&& value?.["1"]?.PointID
|
||||
&& typeof avatarData?.data?.skills?.[value?.["1"]?.PointID] === "number"
|
||||
&& avatarData?.data?.skills?.[value?.["1"]?.PointID] !== 0
|
||||
&& value?.["1"]?.StatusAddList
|
||||
&& value?.["1"].StatusAddList.length > 0) {
|
||||
value?.["1"]?.StatusAddList.forEach((status) => {
|
||||
const statsBase = mappingStats?.[status?.PropertyType]?.baseStat
|
||||
if (statsBase && statsData[statsBase]) {
|
||||
statsData[statsBase].value += calcBonusStatRaw(status?.PropertyType, statsData[statsBase].base, status.Value)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (avatarProfile?.relics && mainAffix && subAffix) {
|
||||
Object.entries(avatarProfile?.relics).forEach(([key, value]) => {
|
||||
const mainAffixMap = mainAffix["5" + key]
|
||||
const subAffixMap = subAffix["5"]
|
||||
if (!mainAffixMap || !subAffixMap) return
|
||||
const mainStats = mappingStats?.[mainAffixMap?.[value.main_affix_id]?.Property]?.baseStat
|
||||
if (mainStats && statsData[mainStats]) {
|
||||
statsData[mainStats].value += calcMainAffixBonusRaw(mainAffixMap?.[value.main_affix_id], value.level, statsData[mainStats].base)
|
||||
}
|
||||
value?.sub_affixes.forEach((subValue) => {
|
||||
const subStats = mappingStats?.[subAffixMap?.[subValue.sub_affix_id]?.Property]?.baseStat
|
||||
if (subStats && statsData[subStats]) {
|
||||
statsData[subStats].value += calcSubAffixBonusRaw(subAffixMap?.[subValue.sub_affix_id], subValue.step, subValue.count, statsData[subStats].base)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (relicEffects && relicEffects.length > 0) {
|
||||
relicEffects.forEach((relic) => {
|
||||
const dataBonus = mapRelicSet?.[relic.key]?.Skills
|
||||
if (!dataBonus || Object.keys(dataBonus).length === 0) return
|
||||
Object.entries(dataBonus || {}).forEach(([key, value]) => {
|
||||
if (relic.count < Number(key)) return
|
||||
value.Bonus.forEach((bonus) => {
|
||||
const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
|
||||
if (statsBase && statsData[statsBase]) {
|
||||
statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return statsData
|
||||
}, [
|
||||
avatarSelected,
|
||||
avatarData,
|
||||
mapAvatar,
|
||||
avatarProfile?.lightcone,
|
||||
avatarProfile?.relics,
|
||||
mapLightCone,
|
||||
mainAffix,
|
||||
subAffix,
|
||||
relicEffects,
|
||||
mapRelicSet,
|
||||
avatarSkillTree
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="col-span-1 md:col-span-2 flex flex-col justify-between py-3">
|
||||
<div className="flex w-full flex-col justify-between gap-y-0.5 text-base">
|
||||
{Object.entries(characterStats || {})?.map(([key, stat], index) => {
|
||||
if (!stat || (key.includes("Add") && stat.value === 0)) return null
|
||||
return (
|
||||
<div key={index} className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center">
|
||||
<NextImage
|
||||
src={`${process.env.CDN_URL}/${stat?.icon}`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
alt="Stat Icon"
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-10 w-10 p-1 mx-1 bg-black/20 rounded-full"
|
||||
/>
|
||||
<div className="font-bold">{stat.name}</div>
|
||||
</div>
|
||||
<div className="ml-3 mr-3 grow border rounded opacity-50" />
|
||||
<div className="flex cursor-default flex-col text-right font-bold">{
|
||||
stat.value ? stat.unit === "%" ? (stat.value * 100).toFixed(stat.round) : stat.value.toFixed(stat.round) : 0
|
||||
}{stat.unit}</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-1 w-full my-2">
|
||||
{relicEffects.map((setEffect, index) => {
|
||||
const relicInfo = mapRelicSet[setEffect.key];
|
||||
if (!relicInfo) return null;
|
||||
return (
|
||||
<div key={index} className="flex w-full flex-row justify-between text-left">
|
||||
<div
|
||||
className="font-bold truncate max-w-full mr-1"
|
||||
style={{
|
||||
fontSize: 'clamp(0.5rem, 2vw, 1rem)'
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, relicInfo.Name),
|
||||
[]
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<span className="black-blur bg-black/20 flex w-5 justify-center rounded px-1.5 py-0.5">{setEffect.count}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 justify-between py-3 text-lg">
|
||||
|
||||
{relicStats?.map((relic, index) => {
|
||||
if (!relic || !avatarSelected) return null
|
||||
return (
|
||||
<RelicShowcase key={index} relic={relic} avatarInfo={avatarSelected} />
|
||||
)
|
||||
})}
|
||||
|
||||
{(!relicStats || !relicStats?.length) && (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="text-center p-6 rounded-lg bg-black/40 backdrop-blur-sm border border-white/10">
|
||||
<span className="text-lg text-gray-400">{transI18n("noRelicEquipped")}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
"use client"
|
||||
import NextImage from "next/image"
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useMemo } from "react";
|
||||
import { calcAffixBonus, calcBaseStatRaw, calcBonusStatRaw, calcMainAffixBonus, calcMainAffixBonusRaw, calcPromotion, calcSubAffixBonusRaw, getLocaleName, replaceByParam } from "@/helper";
|
||||
import { mappingStats } from "@/constant/constant";
|
||||
import RelicShowcase from "../showcaseCard/relicShowcase";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||
|
||||
export default function QuickView() {
|
||||
const { avatars } = useUserDataStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { locale } = useLocaleStore()
|
||||
const { avatarSelected, } = useCurrentDataStore()
|
||||
const { mainAffix, subAffix, mapRelicSet, mapLightCone, mapAvatar } = useDetailDataStore()
|
||||
|
||||
|
||||
const avatarSkillTree = useMemo(() => {
|
||||
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
|
||||
if (avatars[avatarSelected?.ID?.toString()].enhanced) {
|
||||
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
|
||||
}
|
||||
return avatarSelected?.SkillTrees || {}
|
||||
}, [avatarSelected, avatars])
|
||||
|
||||
const avatarData = useMemo(() => {
|
||||
if (!avatarSelected) return
|
||||
return avatars[avatarSelected?.ID?.toString()]
|
||||
}, [avatarSelected, avatars])
|
||||
|
||||
const avatarProfile = useMemo(() => {
|
||||
if (!avatarSelected || !avatarData) return
|
||||
return avatarData?.profileList?.[avatarData?.profileSelect]
|
||||
}, [avatarSelected, avatarData])
|
||||
|
||||
const relicEffects = useMemo(() => {
|
||||
const avatar = avatars[avatarSelected?.ID?.toString() || ""];
|
||||
const relicCount: { [key: string]: number } = {};
|
||||
if (avatar) {
|
||||
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
|
||||
if (relicCount[relic.relic_set_id]) {
|
||||
relicCount[relic.relic_set_id]++;
|
||||
} else {
|
||||
relicCount[relic.relic_set_id] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
const listEffects: { key: string, count: number }[] = [];
|
||||
Object.entries(relicCount).forEach(([key, value]) => {
|
||||
if (value >= 2) {
|
||||
listEffects.push({ key: key, count: value });
|
||||
}
|
||||
});
|
||||
return listEffects;
|
||||
}, [avatars, avatarSelected]);
|
||||
|
||||
const relicStats = useMemo(() => {
|
||||
if (!avatarSelected || !avatarProfile?.relics || !mainAffix || !subAffix) return
|
||||
|
||||
return Object.entries(avatarProfile?.relics).map(([key, value]) => {
|
||||
const mainAffixMap = mainAffix["5" + key]
|
||||
const subAffixMap = subAffix["5"]
|
||||
if (!mainAffixMap || !subAffixMap) return
|
||||
return {
|
||||
img: `${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${value.relic_set_id}_${key}.png`,
|
||||
mainAffix: {
|
||||
property: mainAffixMap?.[value?.main_affix_id]?.Property,
|
||||
level: value?.level,
|
||||
valueAffix: calcMainAffixBonus(mainAffixMap?.[value?.main_affix_id], value?.level),
|
||||
detail: mappingStats?.[mainAffixMap?.[value?.main_affix_id]?.Property]
|
||||
},
|
||||
subAffix: value?.sub_affixes?.map((subValue) => {
|
||||
return {
|
||||
property: subAffixMap?.[subValue?.sub_affix_id]?.Property,
|
||||
valueAffix: calcAffixBonus(subAffixMap?.[subValue?.sub_affix_id], subValue?.step, subValue?.count),
|
||||
detail: mappingStats?.[subAffixMap?.[subValue?.sub_affix_id]?.Property],
|
||||
step: subValue?.step,
|
||||
count: subValue?.count
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}, [avatarSelected, avatarProfile, mainAffix, subAffix])
|
||||
|
||||
const characterStats = useMemo(() => {
|
||||
if (!avatarSelected || !avatarData) return
|
||||
const charPromotion = calcPromotion(avatarData.level)
|
||||
|
||||
const statsData: Record<string, {
|
||||
value: number,
|
||||
base: number,
|
||||
name: string,
|
||||
icon: string,
|
||||
unit: string,
|
||||
round: number
|
||||
}> = {
|
||||
HP: {
|
||||
value: calcBaseStatRaw(
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
|
||||
avatarData.level
|
||||
),
|
||||
base: calcBaseStatRaw(
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
|
||||
avatarData.level
|
||||
),
|
||||
name: "HP",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconMaxHP.png",
|
||||
unit: "",
|
||||
round: 0
|
||||
},
|
||||
ATK: {
|
||||
value: calcBaseStatRaw(
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
|
||||
avatarData.level
|
||||
),
|
||||
base: calcBaseStatRaw(
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
|
||||
avatarData.level
|
||||
),
|
||||
name: "ATK",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconAttack.png",
|
||||
unit: "",
|
||||
round: 0
|
||||
},
|
||||
DEF: {
|
||||
value: calcBaseStatRaw(
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
|
||||
avatarData.level
|
||||
),
|
||||
base: calcBaseStatRaw(
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
|
||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
|
||||
avatarData.level
|
||||
),
|
||||
name: "DEF",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconDefence.png",
|
||||
unit: "",
|
||||
round: 0
|
||||
},
|
||||
SPD: {
|
||||
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
|
||||
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
|
||||
name: "SPD",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconSpeed.png",
|
||||
unit: "",
|
||||
round: 1
|
||||
},
|
||||
CRITRate: {
|
||||
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
|
||||
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
|
||||
name: "CRIT Rate",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconCriticalChance.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
CRITDmg: {
|
||||
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
|
||||
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
|
||||
name: "CRIT DMG",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
BreakEffect: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Break Effect",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconBreakUp.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
EffectRES: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Effect RES",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconStatusResistance.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
EnergyRate: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Energy Rate",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
EffectHitRate: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Effect Hit Rate",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconStatusProbability.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
HealBoost: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Healing Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconHealRatio.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
PhysicalAdd: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Physical Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
FireAdd: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Fire Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
IceAdd: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Ice Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
ThunderAdd: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Thunder Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
WindAdd: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Wind Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
QuantumAdd: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Quantum Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
ImaginaryAdd: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Imaginary Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
},
|
||||
ElationAdd: {
|
||||
value: 0,
|
||||
base: 0,
|
||||
name: "Elation Boost",
|
||||
icon: "spriteoutput/ui/avatar/icon/IconJoy.png",
|
||||
unit: "%",
|
||||
round: 1
|
||||
}
|
||||
}
|
||||
|
||||
if (avatarProfile?.lightcone && mapLightCone[avatarProfile?.lightcone?.item_id]) {
|
||||
const lightconePromotion = calcPromotion(avatarProfile?.lightcone?.level)
|
||||
statsData.HP.value += calcBaseStatRaw(
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd,
|
||||
avatarProfile?.lightcone?.level
|
||||
)
|
||||
statsData.HP.base += calcBaseStatRaw(
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHPAdd,
|
||||
avatarProfile?.lightcone?.level
|
||||
)
|
||||
statsData.ATK.value += calcBaseStatRaw(
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
|
||||
avatarProfile?.lightcone?.level
|
||||
)
|
||||
statsData.ATK.base += calcBaseStatRaw(
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
|
||||
avatarProfile?.lightcone?.level
|
||||
)
|
||||
statsData.DEF.value += calcBaseStatRaw(
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
|
||||
avatarProfile?.lightcone?.level
|
||||
)
|
||||
statsData.DEF.base += calcBaseStatRaw(
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
|
||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
|
||||
avatarProfile?.lightcone?.level
|
||||
)
|
||||
|
||||
const bonusData = mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Skills?.Level?.[avatarProfile?.lightcone.rank]?.Bonus
|
||||
if (bonusData && bonusData.length > 0) {
|
||||
const bonusSpd = bonusData.filter((bonus) => bonus.PropertyType === "BaseSpeed")
|
||||
const bonusOther = bonusData.filter((bonus) => bonus.PropertyType !== "BaseSpeed")
|
||||
bonusSpd.forEach((bonus) => {
|
||||
statsData.SPD.value += bonus.Value
|
||||
statsData.SPD.base += bonus.Value
|
||||
})
|
||||
bonusOther.forEach((bonus) => {
|
||||
const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
|
||||
if (statsBase && statsData[statsBase]) {
|
||||
statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
if (avatarSkillTree) {
|
||||
Object.values(avatarSkillTree).forEach((value) => {
|
||||
if (value?.["1"]
|
||||
&& value?.["1"]?.PointID
|
||||
&& typeof avatarData?.data?.skills?.[value?.["1"]?.PointID] === "number"
|
||||
&& avatarData?.data?.skills?.[value?.["1"]?.PointID] !== 0
|
||||
&& value?.["1"]?.StatusAddList
|
||||
&& value?.["1"].StatusAddList.length > 0) {
|
||||
value?.["1"]?.StatusAddList.forEach((status) => {
|
||||
const statsBase = mappingStats?.[status?.PropertyType]?.baseStat
|
||||
if (statsBase && statsData[statsBase]) {
|
||||
statsData[statsBase].value += calcBonusStatRaw(status?.PropertyType, statsData[statsBase].base, status.Value)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (avatarProfile?.relics && mainAffix && subAffix) {
|
||||
Object.entries(avatarProfile?.relics).forEach(([key, value]) => {
|
||||
const mainAffixMap = mainAffix["5" + key]
|
||||
const subAffixMap = subAffix["5"]
|
||||
if (!mainAffixMap || !subAffixMap) return
|
||||
const mainStats = mappingStats?.[mainAffixMap?.[value.main_affix_id]?.Property]?.baseStat
|
||||
if (mainStats && statsData[mainStats]) {
|
||||
statsData[mainStats].value += calcMainAffixBonusRaw(mainAffixMap?.[value.main_affix_id], value.level, statsData[mainStats].base)
|
||||
}
|
||||
value?.sub_affixes.forEach((subValue) => {
|
||||
const subStats = mappingStats?.[subAffixMap?.[subValue.sub_affix_id]?.Property]?.baseStat
|
||||
if (subStats && statsData[subStats]) {
|
||||
statsData[subStats].value += calcSubAffixBonusRaw(subAffixMap?.[subValue.sub_affix_id], subValue.step, subValue.count, statsData[subStats].base)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if (relicEffects && relicEffects.length > 0) {
|
||||
relicEffects.forEach((relic) => {
|
||||
const dataBonus = mapRelicSet?.[relic.key]?.Skills
|
||||
if (!dataBonus || Object.keys(dataBonus).length === 0) return
|
||||
Object.entries(dataBonus || {}).forEach(([key, value]) => {
|
||||
if (relic.count < Number(key)) return
|
||||
value.Bonus.forEach((bonus) => {
|
||||
const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
|
||||
if (statsBase && statsData[statsBase]) {
|
||||
statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return statsData
|
||||
}, [
|
||||
avatarSelected,
|
||||
avatarData,
|
||||
mapAvatar,
|
||||
avatarProfile?.lightcone,
|
||||
avatarProfile?.relics,
|
||||
mapLightCone,
|
||||
mainAffix,
|
||||
subAffix,
|
||||
relicEffects,
|
||||
mapRelicSet,
|
||||
avatarSkillTree
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="col-span-1 md:col-span-2 flex flex-col justify-between py-3">
|
||||
<div className="flex w-full flex-col justify-between gap-y-0.5 text-base">
|
||||
{Object.entries(characterStats || {})?.map(([key, stat], index) => {
|
||||
if (!stat || (key.includes("Add") && stat.value === 0)) return null
|
||||
return (
|
||||
<div key={index} className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center">
|
||||
<NextImage
|
||||
src={`${process.env.CDN_URL}/${stat?.icon}`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
alt="Stat Icon"
|
||||
width={40}
|
||||
height={40}
|
||||
className="h-10 w-10 p-1 mx-1 bg-black/20 rounded-full"
|
||||
/>
|
||||
<div className="font-bold">{stat.name}</div>
|
||||
</div>
|
||||
<div className="ml-3 mr-3 grow border rounded opacity-50" />
|
||||
<div className="flex cursor-default flex-col text-right font-bold">{
|
||||
stat.value ? stat.unit === "%" ? (stat.value * 100).toFixed(stat.round) : stat.value.toFixed(stat.round) : 0
|
||||
}{stat.unit}</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<hr />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center gap-1 w-full my-2">
|
||||
{relicEffects.map((setEffect, index) => {
|
||||
const relicInfo = mapRelicSet[setEffect.key];
|
||||
if (!relicInfo) return null;
|
||||
return (
|
||||
<div key={index} className="flex w-full flex-row justify-between text-left">
|
||||
<div
|
||||
className="font-bold truncate max-w-full mr-1"
|
||||
style={{
|
||||
fontSize: 'clamp(0.5rem, 2vw, 1rem)'
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, relicInfo.Name),
|
||||
[]
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<span className="black-blur bg-black/20 flex w-5 justify-center rounded px-1.5 py-0.5">{setEffect.count}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 justify-between py-3 text-lg">
|
||||
|
||||
{relicStats?.map((relic, index) => {
|
||||
if (!relic || !avatarSelected) return null
|
||||
return (
|
||||
<RelicShowcase key={index} relic={relic} avatarInfo={avatarSelected} />
|
||||
)
|
||||
})}
|
||||
|
||||
{(!relicStats || !relicStats?.length) && (
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="text-center p-6 rounded-lg bg-black/40 backdrop-blur-sm border border-white/10">
|
||||
<span className="text-lg text-gray-400">{transI18n("noRelicEquipped")}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+498
-498
@@ -1,498 +1,498 @@
|
||||
"use client";
|
||||
import useUserDataStore from '@/stores/userDataStore';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import SelectCustomImage from '../select/customSelectImage';
|
||||
import { calcAffixBonus, calcMainAffixBonus, randomPartition, randomStep, replaceByParam, getLocaleName } from '@/helper';
|
||||
import { mappingStats } from '@/constant/constant';
|
||||
import useModelStore from '@/stores/modelStore';
|
||||
import useRelicMakerStore from '@/stores/relicMakerStore';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import useDetailDataStore from '@/stores/detailDataStore';
|
||||
import useCurrentDataStore from '@/stores/currentDataStore';
|
||||
import { RelicSetDetail, SubAffixData } from '@/types';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import { mappingRelicSlot } from "@/constant/constant";
|
||||
|
||||
export default function RelicMaker() {
|
||||
const { avatars, setAvatars } = useUserDataStore()
|
||||
const { avatarSelected } = useCurrentDataStore()
|
||||
const { setIsOpenRelic } = useModelStore()
|
||||
const { mainAffix, subAffix, mapRelicSet } = useDetailDataStore()
|
||||
const { locale } = useLocaleStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const {
|
||||
selectedRelicSlot,
|
||||
selectedRelicSet,
|
||||
selectedMainStat,
|
||||
listSelectedSubStats,
|
||||
selectedRelicLevel,
|
||||
preSelectedSubStats,
|
||||
setSelectedRelicSet,
|
||||
setSelectedMainStat,
|
||||
setSelectedRelicLevel,
|
||||
setListSelectedSubStats,
|
||||
resetHistory,
|
||||
popHistory,
|
||||
addHistory,
|
||||
} = useRelicMakerStore()
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const relicSets = useMemo(() => {
|
||||
const listSet: Record<string, RelicSetDetail> = {};
|
||||
for (const [key, value] of Object.entries(mapRelicSet || {})) {
|
||||
let isOk = false;
|
||||
for (const key2 of Object.keys(value.Parts)) {
|
||||
if (key2 == mappingRelicSlot?.[selectedRelicSlot]) {
|
||||
isOk = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isOk) {
|
||||
listSet[key] = value;
|
||||
}
|
||||
}
|
||||
return listSet;
|
||||
}, [mapRelicSet , selectedRelicSlot]);
|
||||
|
||||
const subAffixOptions = useMemo(() => {
|
||||
const listSet: Record<string, SubAffixData> = {};
|
||||
const subAffixMap = subAffix["5"];
|
||||
const mainAffixMap = mainAffix["5" + selectedRelicSlot]
|
||||
|
||||
if (Object.keys(subAffixMap || {}).length === 0 || Object.keys(mainAffixMap || {}).length === 0) return listSet;
|
||||
|
||||
for (const [key, value] of Object.entries(subAffixMap)) {
|
||||
if (value.Property !== mainAffixMap[selectedMainStat]?.Property) {
|
||||
listSet[key] = value;
|
||||
}
|
||||
}
|
||||
return listSet;
|
||||
}, [subAffix, mainAffix, selectedRelicSlot, selectedMainStat]);
|
||||
|
||||
useEffect(() => {
|
||||
const subAffixMap = subAffix["5"];
|
||||
const mainAffixMap = mainAffix["5" + selectedRelicSlot];
|
||||
|
||||
if (!subAffixMap || !mainAffixMap) return;
|
||||
|
||||
const mainProp = mainAffixMap[selectedMainStat]?.Property;
|
||||
if (!mainProp) return;
|
||||
|
||||
const newSubAffixes = structuredClone(listSelectedSubStats);
|
||||
let updated = false;
|
||||
|
||||
for (let i = 0; i < newSubAffixes.length; i++) {
|
||||
if (newSubAffixes[i].property === mainProp) {
|
||||
newSubAffixes[i].affixId = "";
|
||||
newSubAffixes[i].property = "";
|
||||
newSubAffixes[i].rollCount = 0;
|
||||
newSubAffixes[i].stepCount = 0;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) setListSelectedSubStats(newSubAffixes);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedMainStat, subAffix, mainAffix, selectedRelicSlot]);
|
||||
|
||||
const exSubAffixOptions = useMemo(() => {
|
||||
const listSet: Record<string, SubAffixData> = {};
|
||||
const subAffixMap = subAffix["5"];
|
||||
const mainAffixMap = mainAffix["5" + selectedRelicSlot];
|
||||
|
||||
if (!subAffixMap || !mainAffixMap) return listSet;
|
||||
|
||||
for (const [key, value] of Object.entries(subAffixMap)) {
|
||||
const subAffix = listSelectedSubStats.find((item) => item.property === value.Property);
|
||||
if (subAffix && value.Property !== mainAffixMap[selectedMainStat]?.Property) {
|
||||
listSet[key] = value;
|
||||
}
|
||||
}
|
||||
return listSet;
|
||||
}, [subAffix, listSelectedSubStats, mainAffix, selectedRelicSlot, selectedMainStat]);
|
||||
|
||||
const effectBonus = useMemo(() => {
|
||||
const affixSet = mainAffix?.["5" + selectedRelicSlot];
|
||||
if (!affixSet) return 0;
|
||||
|
||||
const data = affixSet[selectedMainStat];
|
||||
if (!data) return 0;
|
||||
|
||||
return calcMainAffixBonus(data, selectedRelicLevel);
|
||||
}, [mainAffix, selectedRelicSlot, selectedMainStat, selectedRelicLevel]);
|
||||
|
||||
const handleSubStatChange = (key: string, index: number, rollCount: number, stepCount: number) => {
|
||||
setError("");
|
||||
const newSubAffixes = structuredClone(listSelectedSubStats);
|
||||
if (!subAffixOptions[key]) {
|
||||
newSubAffixes[index].affixId = "";
|
||||
newSubAffixes[index].property = "";
|
||||
newSubAffixes[index].rollCount = rollCount;
|
||||
newSubAffixes[index].stepCount = stepCount;
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
addHistory(index, newSubAffixes[index]);
|
||||
return;
|
||||
}
|
||||
newSubAffixes[index].affixId = key;
|
||||
newSubAffixes[index].property = subAffixOptions[key].Property;
|
||||
newSubAffixes[index].rollCount = rollCount;
|
||||
newSubAffixes[index].stepCount = stepCount;
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
addHistory(index, newSubAffixes[index]);
|
||||
};
|
||||
|
||||
const handlerRollback = (index: number) => {
|
||||
setError("");
|
||||
if (!preSelectedSubStats[index]) return;
|
||||
|
||||
const keys = Object.keys(preSelectedSubStats[index]);
|
||||
if (keys.length <= 1) return;
|
||||
|
||||
const newSubAffixes = structuredClone(listSelectedSubStats);
|
||||
const listHistory = structuredClone(preSelectedSubStats[index]);
|
||||
const secondLastKey = listHistory.length - 2;
|
||||
const preSubAffixes = { ...listHistory[secondLastKey] };
|
||||
newSubAffixes[index].rollCount = preSubAffixes.rollCount;
|
||||
newSubAffixes[index].stepCount = preSubAffixes.stepCount;
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
popHistory(index);
|
||||
};
|
||||
|
||||
const resetSubStat = (index: number) => {
|
||||
const newSubAffixes = structuredClone(listSelectedSubStats);
|
||||
resetHistory(index);
|
||||
newSubAffixes[index].affixId = "";
|
||||
newSubAffixes[index].property = "";
|
||||
newSubAffixes[index].rollCount = 0;
|
||||
newSubAffixes[index].stepCount = 0;
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
};
|
||||
|
||||
const randomizeStats = () => {
|
||||
const newSubAffixes = structuredClone(listSelectedSubStats);
|
||||
const exKeys = Object.keys(exSubAffixOptions);
|
||||
for (let i = 0; i < newSubAffixes.length; i++) {
|
||||
const keys = Object.keys(subAffixOptions).filter((key) => !exKeys.includes(key));
|
||||
const randomKey = keys[Math.floor(Math.random() * keys.length)];
|
||||
exKeys.push(randomKey);
|
||||
const randomValue = subAffixOptions[randomKey];
|
||||
newSubAffixes[i].affixId = randomKey;
|
||||
newSubAffixes[i].property = randomValue.Property;
|
||||
newSubAffixes[i].rollCount = 0;
|
||||
newSubAffixes[i].stepCount = 0;
|
||||
}
|
||||
for (let i = 0; i < newSubAffixes.length; i++) {
|
||||
addHistory(i, newSubAffixes[i]);
|
||||
}
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
|
||||
};
|
||||
|
||||
const randomizeRolls = () => {
|
||||
const newSubAffixes = structuredClone(listSelectedSubStats);
|
||||
const randomRolls = randomPartition(9, listSelectedSubStats.length);
|
||||
for (let i = 0; i < listSelectedSubStats.length; i++) {
|
||||
newSubAffixes[i].rollCount = randomRolls[i];
|
||||
newSubAffixes[i].stepCount = randomStep(randomRolls[i]);
|
||||
}
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
for (let i = 0; i < newSubAffixes.length; i++) {
|
||||
addHistory(i, newSubAffixes[i]);
|
||||
}
|
||||
};
|
||||
|
||||
const handlerSaveRelic = () => {
|
||||
setError("");
|
||||
const avatar = avatars[avatarSelected?.ID?.toString() || ""];
|
||||
if (!selectedRelicSet || !selectedMainStat || !selectedRelicLevel || !selectedRelicSlot) {
|
||||
setError(transI18n("pleaseSelectAllOptions"));
|
||||
return;
|
||||
};
|
||||
|
||||
if (listSelectedSubStats.find((item) => item.affixId === "")) {
|
||||
setError(transI18n("pleaseSelectAllSubStats"));
|
||||
return;
|
||||
};
|
||||
|
||||
if (avatar) {
|
||||
avatar.profileList[avatar.profileSelect].relics[selectedRelicSlot] = {
|
||||
level: selectedRelicLevel,
|
||||
relic_id: Number(`6${selectedRelicSet}${selectedRelicSlot}`),
|
||||
relic_set_id: Number(selectedRelicSet),
|
||||
main_affix_id: Number(selectedMainStat),
|
||||
sub_affixes: listSelectedSubStats.map((item) => {
|
||||
return {
|
||||
sub_affix_id: Number(item.affixId),
|
||||
count: item.rollCount,
|
||||
step: item.stepCount
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
setAvatars({ ...avatars });
|
||||
setIsOpenRelic(false);
|
||||
|
||||
toast.success(transI18n("relicSavedSuccessfully"));
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
||||
{transI18n("relicMaker")}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-2">
|
||||
|
||||
{/* Left Panel */}
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Set Configuration */}
|
||||
<div className="bg-base-100 rounded-xl p-6 border border-slate-700">
|
||||
<h2 className="text-xl font-bold mb-6 text-warning">{transI18n("mainSettings")}</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
{/* Main Stat */}
|
||||
<div>
|
||||
<label className="block text-lg font-medium mb-2">{transI18n("mainStat")}</label>
|
||||
<SelectCustomImage
|
||||
customSet={Object.entries(mainAffix["5" + selectedRelicSlot] || {}).map(([key, value]) => ({
|
||||
value: key,
|
||||
label: mappingStats[value.Property].name + " " + mappingStats[value.Property].unit,
|
||||
imageUrl: `${process.env.CDN_URL}/${mappingStats[value.Property].icon}`
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={selectedMainStat}
|
||||
placeholder={transI18n("selectAMainStat")}
|
||||
setSelectedCustomSet={setSelectedMainStat}
|
||||
/>
|
||||
</div>
|
||||
{/* Relic Set Selection */}
|
||||
<div>
|
||||
<label className="block text-lg font-medium mb-2">{transI18n("set")}</label>
|
||||
<SelectCustomImage
|
||||
customSet={Object.entries(relicSets).map(([key, value]) => ({
|
||||
value: key,
|
||||
label: getLocaleName(locale, value.Name),
|
||||
imageUrl: `${process.env.CDN_URL}/${value.Image.SetIconPath}`
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={selectedRelicSet}
|
||||
placeholder={transI18n("selectASet")}
|
||||
setSelectedCustomSet={setSelectedRelicSet}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Set Bonus Display */}
|
||||
<div className="mb-6 py-4 bg-base-100 rounded-lg">
|
||||
{selectedRelicSet !== "" ? Object.entries(mapRelicSet[selectedRelicSet].Skills).map(([key, value]) => (
|
||||
<div key={key} className="text-blue-300 text-sm mb-1">
|
||||
<span className="text-info font-bold">{key}-Pc:
|
||||
<div
|
||||
className="text-warning leading-relaxed font-bold"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, value.Desc),
|
||||
value.Param || []
|
||||
)
|
||||
}}
|
||||
/> </span>
|
||||
</div>
|
||||
)) : <p className="text-blue-300 text-sm font-bold mb-1">{transI18n("pleaseSelectASet")}</p>}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Rarity */}
|
||||
<div className="grid grid-cols-2 items-center gap-4 mb-6">
|
||||
<label className="block text-lg font-medium mb-2">{transI18n("rarity")}: {5} ⭐</label>
|
||||
<label className="block text-lg font-medium mb-2">{transI18n("effectBonus")}: <span className="text-warning font-bold">{effectBonus}</span></label>
|
||||
</div>
|
||||
|
||||
{/* Level */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-lg font-medium mb-2">{transI18n("level")}</label>
|
||||
<div className="bg-base-200 rounded-lg p-4">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="15"
|
||||
value={selectedRelicLevel}
|
||||
onChange={(e) => setSelectedRelicLevel(parseInt(e.target.value))}
|
||||
className="range range-primary w-full"
|
||||
/>
|
||||
<div className="text-center text-2xl font-bold mt-2">{selectedRelicLevel}</div>
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{error && (
|
||||
<motion.p
|
||||
key="error"
|
||||
initial={{ x: 0 }}
|
||||
animate={{ x: [0, -2, 2, -2, 2, 0] }}
|
||||
transition={{
|
||||
duration: 6,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
repeatDelay: 1.5,
|
||||
}}
|
||||
className="text-error my-2 text-center font-bold"
|
||||
>
|
||||
{error}!
|
||||
</motion.p>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
|
||||
|
||||
{/* Save Button */}
|
||||
<button onClick={handlerSaveRelic} className="btn btn-success w-full">
|
||||
{transI18n("save")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Panel - Sub Stats */}
|
||||
<div className="space-y-4">
|
||||
{/* Total Roll */}
|
||||
<div className="bg-base-100 rounded-xl p-4 border border-slate-700 z-1">
|
||||
<h3 className="text-lg font-bold mb-4">{transI18n("totalRoll")} {listSelectedSubStats.reduce((a, b) => a + b.rollCount, 0)}</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
className="btn btn-outline btn-success sm:btn-sm"
|
||||
onClick={randomizeStats}
|
||||
>
|
||||
{transI18n("randomizeStats")}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline btn-success sm:btn-sm"
|
||||
onClick={randomizeRolls}
|
||||
>
|
||||
{transI18n("randomizeRolls")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{listSelectedSubStats.map((v, index) => (
|
||||
<div key={index} className={`bg-base-100 rounded-xl p-4 border border-slate-700`}>
|
||||
<div className="grid grid-cols-12 gap-2 items-center">
|
||||
|
||||
{/* Stat Selection */}
|
||||
<div className="col-span-8">
|
||||
<SelectCustomImage
|
||||
customSet={Object.entries(subAffixOptions).map(([key, value]) => ({
|
||||
value: key,
|
||||
label: mappingStats[value.Property].name + " " + mappingStats[value.Property].unit,
|
||||
imageUrl: `${process.env.CDN_URL}/${mappingStats[value.Property].icon}`
|
||||
}))}
|
||||
excludeSet={Object.entries(exSubAffixOptions).map(([key, value]) => ({
|
||||
value: key,
|
||||
label: mappingStats[value.Property].name + " " + mappingStats[value.Property].unit,
|
||||
imageUrl: `${process.env.CDN_URL}/${mappingStats[value.Property].icon}`
|
||||
}))}
|
||||
selectedCustomSet={v.affixId}
|
||||
placeholder={transI18n("selectASubStat")}
|
||||
setSelectedCustomSet={(key) => handleSubStatChange(key, index, 0, 0)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Current Value */}
|
||||
<div className="col-span-4 text-center flex items-center justify-center gap-2">
|
||||
<span className="text-2xl font-mono">+{ }</span>
|
||||
<div className="text-xl font-bold text-info">{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount, v.rollCount)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
{/* Up Roll Values */}
|
||||
<div className="col-span-12">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<ChevronUp className="w-4 h-4 text-success" />
|
||||
<span className="text-sm font-semibold text-success">{transI18n("upRoll")}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
<button
|
||||
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 0)}
|
||||
className="btn btn-sm btn-info border-0"
|
||||
>
|
||||
{calcAffixBonus(subAffixOptions[v.affixId], 0, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 1)}
|
||||
className="btn btn-sm btn-info border-0"
|
||||
>
|
||||
{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + 1, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 2)}
|
||||
className="btn btn-sm btn-info border-0"
|
||||
>
|
||||
{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + 2, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Down Roll Values */}
|
||||
<div className="col-span-12">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<ChevronDown className="w-4 h-4 text-error" />
|
||||
<span className="text-sm font-semibold text-error">{transI18n("downRoll")}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
<button
|
||||
onClick={() => handleSubStatChange(v.affixId, index, Math.max(v.rollCount - 1, 0), Math.max(v.stepCount, 0))}
|
||||
className="btn btn-sm btn-info border-0"
|
||||
>
|
||||
{calcAffixBonus(subAffixOptions[v.affixId], 0, Math.max(v.rollCount - 1, 0))}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSubStatChange(v.affixId, index, Math.max(v.rollCount - 1, 0), Math.max(v.stepCount - 1, 0))}
|
||||
className="btn btn-sm btn-info border-0"
|
||||
>
|
||||
{calcAffixBonus(subAffixOptions[v.affixId], Math.max(v.stepCount - 1, 0), Math.max(v.rollCount - 1, 0))}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSubStatChange(v.affixId, index, Math.max(v.rollCount - 1, 0), Math.max(v.stepCount - 2, 0))}
|
||||
className="btn btn-sm btn-info border-0"
|
||||
>
|
||||
{calcAffixBonus(subAffixOptions[v.affixId], Math.max(v.stepCount - 2, 0), Math.max(v.rollCount - 1, 0))}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reset Button & Roll Info */}
|
||||
<div className="col-span-12 text-center w-full">
|
||||
<div className="grid grid-rows-2 gap-1 items-center justify-items-start w-full">
|
||||
<div className="grid grid-cols-2 gap-2 items-center w-full">
|
||||
<button
|
||||
className="btn btn-error btn-sm mb-1"
|
||||
onClick={() => resetSubStat(index)}
|
||||
>
|
||||
{transI18n("reset")}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-warning btn-sm mb-1"
|
||||
onClick={() => handlerRollback(index)}
|
||||
>
|
||||
{transI18n("rollBack")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 items-center w-full">
|
||||
<span className="font-bold">{transI18n("roll")}: <span className="text-info">{v.rollCount}</span></span>
|
||||
<span className="font-bold">{transI18n("step")}: <span className="text-info">{v.stepCount}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
"use client";
|
||||
import useUserDataStore from '@/stores/userDataStore';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import SelectCustomImage from '../select/customSelectImage';
|
||||
import { calcAffixBonus, calcMainAffixBonus, randomPartition, randomStep, replaceByParam, getLocaleName } from '@/helper';
|
||||
import { mappingStats } from '@/constant/constant';
|
||||
import useModelStore from '@/stores/modelStore';
|
||||
import useRelicMakerStore from '@/stores/relicMakerStore';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import useDetailDataStore from '@/stores/detailDataStore';
|
||||
import useCurrentDataStore from '@/stores/currentDataStore';
|
||||
import { RelicSetDetail, SubAffixData } from '@/types';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import { mappingRelicSlot } from "@/constant/constant";
|
||||
|
||||
export default function RelicMaker() {
|
||||
const { avatars, setAvatars } = useUserDataStore()
|
||||
const { avatarSelected } = useCurrentDataStore()
|
||||
const { setIsOpenRelic } = useModelStore()
|
||||
const { mainAffix, subAffix, mapRelicSet } = useDetailDataStore()
|
||||
const { locale } = useLocaleStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const {
|
||||
selectedRelicSlot,
|
||||
selectedRelicSet,
|
||||
selectedMainStat,
|
||||
listSelectedSubStats,
|
||||
selectedRelicLevel,
|
||||
preSelectedSubStats,
|
||||
setSelectedRelicSet,
|
||||
setSelectedMainStat,
|
||||
setSelectedRelicLevel,
|
||||
setListSelectedSubStats,
|
||||
resetHistory,
|
||||
popHistory,
|
||||
addHistory,
|
||||
} = useRelicMakerStore()
|
||||
const [error, setError] = useState<string>("");
|
||||
|
||||
const relicSets = useMemo(() => {
|
||||
const listSet: Record<string, RelicSetDetail> = {};
|
||||
for (const [key, value] of Object.entries(mapRelicSet || {})) {
|
||||
let isOk = false;
|
||||
for (const key2 of Object.keys(value.Parts)) {
|
||||
if (key2 == mappingRelicSlot?.[selectedRelicSlot]) {
|
||||
isOk = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isOk) {
|
||||
listSet[key] = value;
|
||||
}
|
||||
}
|
||||
return listSet;
|
||||
}, [mapRelicSet , selectedRelicSlot]);
|
||||
|
||||
const subAffixOptions = useMemo(() => {
|
||||
const listSet: Record<string, SubAffixData> = {};
|
||||
const subAffixMap = subAffix["5"];
|
||||
const mainAffixMap = mainAffix["5" + selectedRelicSlot]
|
||||
|
||||
if (Object.keys(subAffixMap || {}).length === 0 || Object.keys(mainAffixMap || {}).length === 0) return listSet;
|
||||
|
||||
for (const [key, value] of Object.entries(subAffixMap)) {
|
||||
if (value.Property !== mainAffixMap[selectedMainStat]?.Property) {
|
||||
listSet[key] = value;
|
||||
}
|
||||
}
|
||||
return listSet;
|
||||
}, [subAffix, mainAffix, selectedRelicSlot, selectedMainStat]);
|
||||
|
||||
useEffect(() => {
|
||||
const subAffixMap = subAffix["5"];
|
||||
const mainAffixMap = mainAffix["5" + selectedRelicSlot];
|
||||
|
||||
if (!subAffixMap || !mainAffixMap) return;
|
||||
|
||||
const mainProp = mainAffixMap[selectedMainStat]?.Property;
|
||||
if (!mainProp) return;
|
||||
|
||||
const newSubAffixes = structuredClone(listSelectedSubStats);
|
||||
let updated = false;
|
||||
|
||||
for (let i = 0; i < newSubAffixes.length; i++) {
|
||||
if (newSubAffixes[i].property === mainProp) {
|
||||
newSubAffixes[i].affixId = "";
|
||||
newSubAffixes[i].property = "";
|
||||
newSubAffixes[i].rollCount = 0;
|
||||
newSubAffixes[i].stepCount = 0;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) setListSelectedSubStats(newSubAffixes);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedMainStat, subAffix, mainAffix, selectedRelicSlot]);
|
||||
|
||||
const exSubAffixOptions = useMemo(() => {
|
||||
const listSet: Record<string, SubAffixData> = {};
|
||||
const subAffixMap = subAffix["5"];
|
||||
const mainAffixMap = mainAffix["5" + selectedRelicSlot];
|
||||
|
||||
if (!subAffixMap || !mainAffixMap) return listSet;
|
||||
|
||||
for (const [key, value] of Object.entries(subAffixMap)) {
|
||||
const subAffix = listSelectedSubStats.find((item) => item.property === value.Property);
|
||||
if (subAffix && value.Property !== mainAffixMap[selectedMainStat]?.Property) {
|
||||
listSet[key] = value;
|
||||
}
|
||||
}
|
||||
return listSet;
|
||||
}, [subAffix, listSelectedSubStats, mainAffix, selectedRelicSlot, selectedMainStat]);
|
||||
|
||||
const effectBonus = useMemo(() => {
|
||||
const affixSet = mainAffix?.["5" + selectedRelicSlot];
|
||||
if (!affixSet) return 0;
|
||||
|
||||
const data = affixSet[selectedMainStat];
|
||||
if (!data) return 0;
|
||||
|
||||
return calcMainAffixBonus(data, selectedRelicLevel);
|
||||
}, [mainAffix, selectedRelicSlot, selectedMainStat, selectedRelicLevel]);
|
||||
|
||||
const handleSubStatChange = (key: string, index: number, rollCount: number, stepCount: number) => {
|
||||
setError("");
|
||||
const newSubAffixes = structuredClone(listSelectedSubStats);
|
||||
if (!subAffixOptions[key]) {
|
||||
newSubAffixes[index].affixId = "";
|
||||
newSubAffixes[index].property = "";
|
||||
newSubAffixes[index].rollCount = rollCount;
|
||||
newSubAffixes[index].stepCount = stepCount;
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
addHistory(index, newSubAffixes[index]);
|
||||
return;
|
||||
}
|
||||
newSubAffixes[index].affixId = key;
|
||||
newSubAffixes[index].property = subAffixOptions[key].Property;
|
||||
newSubAffixes[index].rollCount = rollCount;
|
||||
newSubAffixes[index].stepCount = stepCount;
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
addHistory(index, newSubAffixes[index]);
|
||||
};
|
||||
|
||||
const handlerRollback = (index: number) => {
|
||||
setError("");
|
||||
if (!preSelectedSubStats[index]) return;
|
||||
|
||||
const keys = Object.keys(preSelectedSubStats[index]);
|
||||
if (keys.length <= 1) return;
|
||||
|
||||
const newSubAffixes = structuredClone(listSelectedSubStats);
|
||||
const listHistory = structuredClone(preSelectedSubStats[index]);
|
||||
const secondLastKey = listHistory.length - 2;
|
||||
const preSubAffixes = { ...listHistory[secondLastKey] };
|
||||
newSubAffixes[index].rollCount = preSubAffixes.rollCount;
|
||||
newSubAffixes[index].stepCount = preSubAffixes.stepCount;
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
popHistory(index);
|
||||
};
|
||||
|
||||
const resetSubStat = (index: number) => {
|
||||
const newSubAffixes = structuredClone(listSelectedSubStats);
|
||||
resetHistory(index);
|
||||
newSubAffixes[index].affixId = "";
|
||||
newSubAffixes[index].property = "";
|
||||
newSubAffixes[index].rollCount = 0;
|
||||
newSubAffixes[index].stepCount = 0;
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
};
|
||||
|
||||
const randomizeStats = () => {
|
||||
const newSubAffixes = structuredClone(listSelectedSubStats);
|
||||
const exKeys = Object.keys(exSubAffixOptions);
|
||||
for (let i = 0; i < newSubAffixes.length; i++) {
|
||||
const keys = Object.keys(subAffixOptions).filter((key) => !exKeys.includes(key));
|
||||
const randomKey = keys[Math.floor(Math.random() * keys.length)];
|
||||
exKeys.push(randomKey);
|
||||
const randomValue = subAffixOptions[randomKey];
|
||||
newSubAffixes[i].affixId = randomKey;
|
||||
newSubAffixes[i].property = randomValue.Property;
|
||||
newSubAffixes[i].rollCount = 0;
|
||||
newSubAffixes[i].stepCount = 0;
|
||||
}
|
||||
for (let i = 0; i < newSubAffixes.length; i++) {
|
||||
addHistory(i, newSubAffixes[i]);
|
||||
}
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
|
||||
};
|
||||
|
||||
const randomizeRolls = () => {
|
||||
const newSubAffixes = structuredClone(listSelectedSubStats);
|
||||
const randomRolls = randomPartition(9, listSelectedSubStats.length);
|
||||
for (let i = 0; i < listSelectedSubStats.length; i++) {
|
||||
newSubAffixes[i].rollCount = randomRolls[i];
|
||||
newSubAffixes[i].stepCount = randomStep(randomRolls[i]);
|
||||
}
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
for (let i = 0; i < newSubAffixes.length; i++) {
|
||||
addHistory(i, newSubAffixes[i]);
|
||||
}
|
||||
};
|
||||
|
||||
const handlerSaveRelic = () => {
|
||||
setError("");
|
||||
const avatar = avatars[avatarSelected?.ID?.toString() || ""];
|
||||
if (!selectedRelicSet || !selectedMainStat || !selectedRelicLevel || !selectedRelicSlot) {
|
||||
setError(transI18n("pleaseSelectAllOptions"));
|
||||
return;
|
||||
};
|
||||
|
||||
if (listSelectedSubStats.find((item) => item.affixId === "")) {
|
||||
setError(transI18n("pleaseSelectAllSubStats"));
|
||||
return;
|
||||
};
|
||||
|
||||
if (avatar) {
|
||||
avatar.profileList[avatar.profileSelect].relics[selectedRelicSlot] = {
|
||||
level: selectedRelicLevel,
|
||||
relic_id: Number(`6${selectedRelicSet}${selectedRelicSlot}`),
|
||||
relic_set_id: Number(selectedRelicSet),
|
||||
main_affix_id: Number(selectedMainStat),
|
||||
sub_affixes: listSelectedSubStats.map((item) => {
|
||||
return {
|
||||
sub_affix_id: Number(item.affixId),
|
||||
count: item.rollCount,
|
||||
step: item.stepCount
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
setAvatars({ ...avatars });
|
||||
setIsOpenRelic(false);
|
||||
|
||||
toast.success(transI18n("relicSavedSuccessfully"));
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
||||
{transI18n("relicMaker")}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-2">
|
||||
|
||||
{/* Left Panel */}
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Set Configuration */}
|
||||
<div className="bg-base-100 rounded-xl p-6 border border-slate-700">
|
||||
<h2 className="text-xl font-bold mb-6 text-warning">{transI18n("mainSettings")}</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
{/* Main Stat */}
|
||||
<div>
|
||||
<label className="block text-lg font-medium mb-2">{transI18n("mainStat")}</label>
|
||||
<SelectCustomImage
|
||||
customSet={Object.entries(mainAffix["5" + selectedRelicSlot] || {}).map(([key, value]) => ({
|
||||
value: key,
|
||||
label: mappingStats[value.Property].name + " " + mappingStats[value.Property].unit,
|
||||
imageUrl: `${process.env.CDN_URL}/${mappingStats[value.Property].icon}`
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={selectedMainStat}
|
||||
placeholder={transI18n("selectAMainStat")}
|
||||
setSelectedCustomSet={setSelectedMainStat}
|
||||
/>
|
||||
</div>
|
||||
{/* Relic Set Selection */}
|
||||
<div>
|
||||
<label className="block text-lg font-medium mb-2">{transI18n("set")}</label>
|
||||
<SelectCustomImage
|
||||
customSet={Object.entries(relicSets).map(([key, value]) => ({
|
||||
value: key,
|
||||
label: getLocaleName(locale, value.Name),
|
||||
imageUrl: `${process.env.CDN_URL}/${value.Image.SetIconPath}`
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={selectedRelicSet}
|
||||
placeholder={transI18n("selectASet")}
|
||||
setSelectedCustomSet={setSelectedRelicSet}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Set Bonus Display */}
|
||||
<div className="mb-6 py-4 bg-base-100 rounded-lg">
|
||||
{selectedRelicSet !== "" ? Object.entries(mapRelicSet[selectedRelicSet].Skills).map(([key, value]) => (
|
||||
<div key={key} className="text-blue-300 text-sm mb-1">
|
||||
<span className="text-info font-bold">{key}-Pc:
|
||||
<div
|
||||
className="text-warning leading-relaxed font-bold"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, value.Desc),
|
||||
value.Param || []
|
||||
)
|
||||
}}
|
||||
/> </span>
|
||||
</div>
|
||||
)) : <p className="text-blue-300 text-sm font-bold mb-1">{transI18n("pleaseSelectASet")}</p>}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Rarity */}
|
||||
<div className="grid grid-cols-2 items-center gap-4 mb-6">
|
||||
<label className="block text-lg font-medium mb-2">{transI18n("rarity")}: {5} ⭐</label>
|
||||
<label className="block text-lg font-medium mb-2">{transI18n("effectBonus")}: <span className="text-warning font-bold">{effectBonus}</span></label>
|
||||
</div>
|
||||
|
||||
{/* Level */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-lg font-medium mb-2">{transI18n("level")}</label>
|
||||
<div className="bg-base-200 rounded-lg p-4">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="15"
|
||||
value={selectedRelicLevel}
|
||||
onChange={(e) => setSelectedRelicLevel(parseInt(e.target.value))}
|
||||
className="range range-primary w-full"
|
||||
/>
|
||||
<div className="text-center text-2xl font-bold mt-2">{selectedRelicLevel}</div>
|
||||
</div>
|
||||
</div>
|
||||
<AnimatePresence>
|
||||
{error && (
|
||||
<motion.p
|
||||
key="error"
|
||||
initial={{ x: 0 }}
|
||||
animate={{ x: [0, -2, 2, -2, 2, 0] }}
|
||||
transition={{
|
||||
duration: 6,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
repeatDelay: 1.5,
|
||||
}}
|
||||
className="text-error my-2 text-center font-bold"
|
||||
>
|
||||
{error}!
|
||||
</motion.p>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
|
||||
|
||||
{/* Save Button */}
|
||||
<button onClick={handlerSaveRelic} className="btn btn-success w-full">
|
||||
{transI18n("save")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Panel - Sub Stats */}
|
||||
<div className="space-y-4">
|
||||
{/* Total Roll */}
|
||||
<div className="bg-base-100 rounded-xl p-4 border border-slate-700 z-1">
|
||||
<h3 className="text-lg font-bold mb-4">{transI18n("totalRoll")} {listSelectedSubStats.reduce((a, b) => a + b.rollCount, 0)}</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<button
|
||||
className="btn btn-outline btn-success sm:btn-sm"
|
||||
onClick={randomizeStats}
|
||||
>
|
||||
{transI18n("randomizeStats")}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline btn-success sm:btn-sm"
|
||||
onClick={randomizeRolls}
|
||||
>
|
||||
{transI18n("randomizeRolls")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{listSelectedSubStats.map((v, index) => (
|
||||
<div key={index} className={`bg-base-100 rounded-xl p-4 border border-slate-700`}>
|
||||
<div className="grid grid-cols-12 gap-2 items-center">
|
||||
|
||||
{/* Stat Selection */}
|
||||
<div className="col-span-8">
|
||||
<SelectCustomImage
|
||||
customSet={Object.entries(subAffixOptions).map(([key, value]) => ({
|
||||
value: key,
|
||||
label: mappingStats[value.Property].name + " " + mappingStats[value.Property].unit,
|
||||
imageUrl: `${process.env.CDN_URL}/${mappingStats[value.Property].icon}`
|
||||
}))}
|
||||
excludeSet={Object.entries(exSubAffixOptions).map(([key, value]) => ({
|
||||
value: key,
|
||||
label: mappingStats[value.Property].name + " " + mappingStats[value.Property].unit,
|
||||
imageUrl: `${process.env.CDN_URL}/${mappingStats[value.Property].icon}`
|
||||
}))}
|
||||
selectedCustomSet={v.affixId}
|
||||
placeholder={transI18n("selectASubStat")}
|
||||
setSelectedCustomSet={(key) => handleSubStatChange(key, index, 0, 0)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Current Value */}
|
||||
<div className="col-span-4 text-center flex items-center justify-center gap-2">
|
||||
<span className="text-2xl font-mono">+{ }</span>
|
||||
<div className="text-xl font-bold text-info">{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount, v.rollCount)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
{/* Up Roll Values */}
|
||||
<div className="col-span-12">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<ChevronUp className="w-4 h-4 text-success" />
|
||||
<span className="text-sm font-semibold text-success">{transI18n("upRoll")}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
<button
|
||||
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 0)}
|
||||
className="btn btn-sm btn-info border-0"
|
||||
>
|
||||
{calcAffixBonus(subAffixOptions[v.affixId], 0, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 1)}
|
||||
className="btn btn-sm btn-info border-0"
|
||||
>
|
||||
{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + 1, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 2)}
|
||||
className="btn btn-sm btn-info border-0"
|
||||
>
|
||||
{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + 2, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Down Roll Values */}
|
||||
<div className="col-span-12">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<ChevronDown className="w-4 h-4 text-error" />
|
||||
<span className="text-sm font-semibold text-error">{transI18n("downRoll")}</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
<button
|
||||
onClick={() => handleSubStatChange(v.affixId, index, Math.max(v.rollCount - 1, 0), Math.max(v.stepCount, 0))}
|
||||
className="btn btn-sm btn-info border-0"
|
||||
>
|
||||
{calcAffixBonus(subAffixOptions[v.affixId], 0, Math.max(v.rollCount - 1, 0))}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSubStatChange(v.affixId, index, Math.max(v.rollCount - 1, 0), Math.max(v.stepCount - 1, 0))}
|
||||
className="btn btn-sm btn-info border-0"
|
||||
>
|
||||
{calcAffixBonus(subAffixOptions[v.affixId], Math.max(v.stepCount - 1, 0), Math.max(v.rollCount - 1, 0))}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSubStatChange(v.affixId, index, Math.max(v.rollCount - 1, 0), Math.max(v.stepCount - 2, 0))}
|
||||
className="btn btn-sm btn-info border-0"
|
||||
>
|
||||
{calcAffixBonus(subAffixOptions[v.affixId], Math.max(v.stepCount - 2, 0), Math.max(v.rollCount - 1, 0))}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reset Button & Roll Info */}
|
||||
<div className="col-span-12 text-center w-full">
|
||||
<div className="grid grid-rows-2 gap-1 items-center justify-items-start w-full">
|
||||
<div className="grid grid-cols-2 gap-2 items-center w-full">
|
||||
<button
|
||||
className="btn btn-error btn-sm mb-1"
|
||||
onClick={() => resetSubStat(index)}
|
||||
>
|
||||
{transI18n("reset")}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-warning btn-sm mb-1"
|
||||
onClick={() => handlerRollback(index)}
|
||||
>
|
||||
{transI18n("rollBack")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 items-center w-full">
|
||||
<span className="font-bold">{transI18n("roll")}: <span className="text-info">{v.rollCount}</span></span>
|
||||
<span className="font-bold">{transI18n("step")}: <span className="text-info">{v.stepCount}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+366
-366
@@ -1,366 +1,366 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
"use client";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import RelicMaker from "../relicBar";
|
||||
import { motion } from "framer-motion";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { useTranslations } from "next-intl";
|
||||
import RelicCard from "../card/relicCard";
|
||||
import useModelStore from '@/stores/modelStore';
|
||||
import { replaceByParam } from '@/helper';
|
||||
import useRelicMakerStore from '@/stores/relicMakerStore';
|
||||
import QuickView from "../quickView";
|
||||
import { ModalConfig } from "@/types";
|
||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
import { getLocaleName } from '@/helper';
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
|
||||
export default function RelicsInfo() {
|
||||
const { avatars, setAvatars } = useUserDataStore()
|
||||
const {
|
||||
setSelectedRelicSlot,
|
||||
selectedRelicSlot,
|
||||
setSelectedMainStat,
|
||||
setSelectedRelicSet,
|
||||
setSelectedRelicLevel,
|
||||
setListSelectedSubStats,
|
||||
resetHistory,
|
||||
resetSubStat,
|
||||
listSelectedSubStats,
|
||||
} = useRelicMakerStore()
|
||||
|
||||
const {
|
||||
isOpenRelic,
|
||||
setIsOpenRelic,
|
||||
isOpenQuickView,
|
||||
setIsOpenQuickView
|
||||
} = useModelStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { avatarSelected } = useCurrentDataStore()
|
||||
const { mapRelicSet, subAffix } = useDetailDataStore()
|
||||
const { locale } = useLocaleStore()
|
||||
|
||||
const handleShow = (modalId: string) => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
modal.showModal();
|
||||
}
|
||||
};
|
||||
|
||||
// Close modal handler
|
||||
const handleCloseModal = (modalId: string) => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
modal.close();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const getRelic = useCallback((slot: string) => {
|
||||
const avatar = avatars[avatarSelected?.ID.toString() || ""];
|
||||
if (avatar) {
|
||||
return avatar.profileList[avatar.profileSelect]?.relics[slot] || null;
|
||||
}
|
||||
return null;
|
||||
}, [avatars, avatarSelected]);
|
||||
|
||||
const handlerDeleteRelic = (slot: string) => {
|
||||
const avatar = avatars[avatarSelected?.ID.toString() || ""];
|
||||
if (avatar) {
|
||||
delete avatar.profileList[avatar.profileSelect].relics[slot]
|
||||
setAvatars({ ...avatars });
|
||||
}
|
||||
}
|
||||
|
||||
const handlerChangeRelic = (slot: string) => {
|
||||
const relic = getRelic(slot)
|
||||
setSelectedRelicSlot(slot)
|
||||
resetSubStat()
|
||||
resetHistory(null)
|
||||
if (relic) {
|
||||
setSelectedMainStat(relic.main_affix_id.toString())
|
||||
setSelectedRelicSet(relic.relic_set_id.toString())
|
||||
setSelectedRelicLevel(relic.level)
|
||||
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
|
||||
relic.sub_affixes.forEach((item, index) => {
|
||||
newSubAffixes[index].affixId = item.sub_affix_id.toString();
|
||||
newSubAffixes[index].property = subAffix["5"][item.sub_affix_id.toString()]?.Property || "";
|
||||
newSubAffixes[index].rollCount = item.count || 0;
|
||||
newSubAffixes[index].stepCount = item.step || 0;
|
||||
})
|
||||
setListSelectedSubStats(newSubAffixes)
|
||||
} else {
|
||||
setSelectedMainStat("")
|
||||
setSelectedRelicSet("")
|
||||
setSelectedRelicLevel(15)
|
||||
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
|
||||
newSubAffixes.forEach((item) => {
|
||||
item.affixId = ""
|
||||
item.property = ""
|
||||
item.rollCount = 0
|
||||
item.stepCount = 0
|
||||
})
|
||||
|
||||
setListSelectedSubStats(newSubAffixes)
|
||||
}
|
||||
setIsOpenRelic(true)
|
||||
handleShow("action_detail_modal")
|
||||
}
|
||||
|
||||
const relicEffects = useMemo(() => {
|
||||
const avatar = avatars[avatarSelected?.ID.toString() || ""];
|
||||
const relicCount: { [key: string]: number } = {};
|
||||
if (avatar) {
|
||||
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
|
||||
if (relicCount[relic.relic_set_id]) {
|
||||
relicCount[relic.relic_set_id]++;
|
||||
} else {
|
||||
relicCount[relic.relic_set_id] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
const listEffects: { key: string, count: number }[] = [];
|
||||
Object.entries(relicCount).forEach(([key, value]) => {
|
||||
if (value >= 2) {
|
||||
listEffects.push({ key: key, count: value });
|
||||
}
|
||||
});
|
||||
return listEffects;
|
||||
}, [avatars, avatarSelected]);
|
||||
|
||||
const modalConfigs: ModalConfig[] = [
|
||||
{
|
||||
id: "action_detail_modal",
|
||||
title: "",
|
||||
isOpen: isOpenRelic,
|
||||
onClose: () => {
|
||||
setIsOpenRelic(false)
|
||||
handleCloseModal("action_detail_modal")
|
||||
},
|
||||
content: <RelicMaker />
|
||||
},
|
||||
{
|
||||
id: "quick_view_modal",
|
||||
title: transI18n("quickView").toUpperCase(),
|
||||
isOpen: isOpenQuickView,
|
||||
onClose: () => {
|
||||
setIsOpenQuickView(false)
|
||||
handleCloseModal("quick_view_modal")
|
||||
},
|
||||
content: <QuickView />
|
||||
}
|
||||
]
|
||||
|
||||
// Handle ESC key to close modal
|
||||
useEffect(() => {
|
||||
for (const item of modalConfigs) {
|
||||
if (!item?.isOpen) {
|
||||
handleCloseModal(item?.id || "")
|
||||
}
|
||||
}
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
for (const item of modalConfigs) {
|
||||
handleCloseModal(item?.id || "")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleEscKey);
|
||||
return () => window.removeEventListener('keydown', handleEscKey);
|
||||
}, [isOpenRelic]);
|
||||
|
||||
return (
|
||||
<div className="max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
{/* Left Section - Items Grid */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
|
||||
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
|
||||
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
|
||||
{transI18n("relics")}
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
|
||||
{["1", "2", "3", "4", "5", "6"].map((item, index) => (
|
||||
<div key={index} className="relative group">
|
||||
<div
|
||||
onClick={() => {
|
||||
if (item === selectedRelicSlot) {
|
||||
setSelectedRelicSlot("")
|
||||
} else {
|
||||
setSelectedRelicSlot(item)
|
||||
}
|
||||
handlerChangeRelic(item)
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<RelicCard
|
||||
slot={item}
|
||||
avatarId={avatarSelected?.ID.toString() || ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handlerChangeRelic(item)
|
||||
}}
|
||||
className="btn btn-info p-1.5 rounded-full shadow-lg transition-colors duration-200"
|
||||
title={transI18n("changeRelic")}
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{getRelic(item) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${item}?`)) {
|
||||
handlerDeleteRelic(item)
|
||||
}
|
||||
}}
|
||||
className="btn btn-error p-1.5 rounded-full shadow-lg transition-colors duration-200"
|
||||
title={transI18n("deleteRelic")}
|
||||
>
|
||||
<svg className="w-3 h-3" 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>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 mt-10">
|
||||
<button
|
||||
disabled={!selectedRelicSlot}
|
||||
onClick={() => {
|
||||
handlerChangeRelic(selectedRelicSlot)
|
||||
}}
|
||||
className="btn btn-info"
|
||||
>
|
||||
{transI18n("changeRelic")}
|
||||
</button>
|
||||
<button
|
||||
disabled={!selectedRelicSlot}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${selectedRelicSlot}?`)) {
|
||||
handlerDeleteRelic(selectedRelicSlot)
|
||||
}
|
||||
}}
|
||||
className="btn btn-error"
|
||||
>
|
||||
{transI18n("deleteRelic")}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsOpenQuickView(true)
|
||||
handleShow("quick_view_modal")
|
||||
}}
|
||||
className="btn btn-info w-full mt-2"
|
||||
>
|
||||
{transI18n("quickView")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Section - Stats and Set Effects */}
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Set Effects Panel */}
|
||||
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
|
||||
<h3 className="flex items-center gap-2 text-xl font-bold mb-4 text-base-content">
|
||||
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
|
||||
{transI18n("setEffects")}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
{relicEffects.map((setEffect, index) => {
|
||||
const relicInfo = mapRelicSet[setEffect.key];
|
||||
if (!relicInfo) return null;
|
||||
return (
|
||||
<div key={index} className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="font-bold text-warning"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, relicInfo.Name),
|
||||
[]
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{setEffect.count && (
|
||||
<span className={`text-sm text-info`}>
|
||||
({setEffect.count})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 pl-4">
|
||||
{Object.entries(relicInfo.Skills).map(([requireNum, value]) => {
|
||||
if (Number(requireNum) > Number(setEffect.count)) return null;
|
||||
return (
|
||||
<div key={requireNum} className="space-y-1">
|
||||
<div className={`font-medium text-success`}>
|
||||
{requireNum}-PC:
|
||||
</div>
|
||||
<div
|
||||
className="text-sm text-base-content/80 leading-relaxed pl-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, value.Desc),
|
||||
value.Param || []
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{modalConfigs.map(({ id, title, onClose, content }) => (
|
||||
<dialog key={id} id={id} className="modal">
|
||||
<div className="modal-box w-11/12 max-w-[90%] max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
|
||||
<div className="sticky top-0 z-10">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1, rotate: 90 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
|
||||
onClick={onClose}
|
||||
>
|
||||
✕
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
{title && (
|
||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{content}
|
||||
</div>
|
||||
</dialog>
|
||||
))}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
"use client";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import RelicMaker from "../relicBar";
|
||||
import { motion } from "framer-motion";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { useTranslations } from "next-intl";
|
||||
import RelicCard from "../card/relicCard";
|
||||
import useModelStore from '@/stores/modelStore';
|
||||
import { replaceByParam } from '@/helper';
|
||||
import useRelicMakerStore from '@/stores/relicMakerStore';
|
||||
import QuickView from "../quickView";
|
||||
import { ModalConfig } from "@/types";
|
||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
import { getLocaleName } from '@/helper';
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
|
||||
export default function RelicsInfo() {
|
||||
const { avatars, setAvatars } = useUserDataStore()
|
||||
const {
|
||||
setSelectedRelicSlot,
|
||||
selectedRelicSlot,
|
||||
setSelectedMainStat,
|
||||
setSelectedRelicSet,
|
||||
setSelectedRelicLevel,
|
||||
setListSelectedSubStats,
|
||||
resetHistory,
|
||||
resetSubStat,
|
||||
listSelectedSubStats,
|
||||
} = useRelicMakerStore()
|
||||
|
||||
const {
|
||||
isOpenRelic,
|
||||
setIsOpenRelic,
|
||||
isOpenQuickView,
|
||||
setIsOpenQuickView
|
||||
} = useModelStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { avatarSelected } = useCurrentDataStore()
|
||||
const { mapRelicSet, subAffix } = useDetailDataStore()
|
||||
const { locale } = useLocaleStore()
|
||||
|
||||
const handleShow = (modalId: string) => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
modal.showModal();
|
||||
}
|
||||
};
|
||||
|
||||
// Close modal handler
|
||||
const handleCloseModal = (modalId: string) => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
modal.close();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const getRelic = useCallback((slot: string) => {
|
||||
const avatar = avatars[avatarSelected?.ID.toString() || ""];
|
||||
if (avatar) {
|
||||
return avatar.profileList[avatar.profileSelect]?.relics[slot] || null;
|
||||
}
|
||||
return null;
|
||||
}, [avatars, avatarSelected]);
|
||||
|
||||
const handlerDeleteRelic = (slot: string) => {
|
||||
const avatar = avatars[avatarSelected?.ID.toString() || ""];
|
||||
if (avatar) {
|
||||
delete avatar.profileList[avatar.profileSelect].relics[slot]
|
||||
setAvatars({ ...avatars });
|
||||
}
|
||||
}
|
||||
|
||||
const handlerChangeRelic = (slot: string) => {
|
||||
const relic = getRelic(slot)
|
||||
setSelectedRelicSlot(slot)
|
||||
resetSubStat()
|
||||
resetHistory(null)
|
||||
if (relic) {
|
||||
setSelectedMainStat(relic.main_affix_id.toString())
|
||||
setSelectedRelicSet(relic.relic_set_id.toString())
|
||||
setSelectedRelicLevel(relic.level)
|
||||
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
|
||||
relic.sub_affixes.forEach((item, index) => {
|
||||
newSubAffixes[index].affixId = item.sub_affix_id.toString();
|
||||
newSubAffixes[index].property = subAffix["5"][item.sub_affix_id.toString()]?.Property || "";
|
||||
newSubAffixes[index].rollCount = item.count || 0;
|
||||
newSubAffixes[index].stepCount = item.step || 0;
|
||||
})
|
||||
setListSelectedSubStats(newSubAffixes)
|
||||
} else {
|
||||
setSelectedMainStat("")
|
||||
setSelectedRelicSet("")
|
||||
setSelectedRelicLevel(15)
|
||||
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
|
||||
newSubAffixes.forEach((item) => {
|
||||
item.affixId = ""
|
||||
item.property = ""
|
||||
item.rollCount = 0
|
||||
item.stepCount = 0
|
||||
})
|
||||
|
||||
setListSelectedSubStats(newSubAffixes)
|
||||
}
|
||||
setIsOpenRelic(true)
|
||||
handleShow("action_detail_modal")
|
||||
}
|
||||
|
||||
const relicEffects = useMemo(() => {
|
||||
const avatar = avatars[avatarSelected?.ID.toString() || ""];
|
||||
const relicCount: { [key: string]: number } = {};
|
||||
if (avatar) {
|
||||
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
|
||||
if (relicCount[relic.relic_set_id]) {
|
||||
relicCount[relic.relic_set_id]++;
|
||||
} else {
|
||||
relicCount[relic.relic_set_id] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
const listEffects: { key: string, count: number }[] = [];
|
||||
Object.entries(relicCount).forEach(([key, value]) => {
|
||||
if (value >= 2) {
|
||||
listEffects.push({ key: key, count: value });
|
||||
}
|
||||
});
|
||||
return listEffects;
|
||||
}, [avatars, avatarSelected]);
|
||||
|
||||
const modalConfigs: ModalConfig[] = [
|
||||
{
|
||||
id: "action_detail_modal",
|
||||
title: "",
|
||||
isOpen: isOpenRelic,
|
||||
onClose: () => {
|
||||
setIsOpenRelic(false)
|
||||
handleCloseModal("action_detail_modal")
|
||||
},
|
||||
content: <RelicMaker />
|
||||
},
|
||||
{
|
||||
id: "quick_view_modal",
|
||||
title: transI18n("quickView").toUpperCase(),
|
||||
isOpen: isOpenQuickView,
|
||||
onClose: () => {
|
||||
setIsOpenQuickView(false)
|
||||
handleCloseModal("quick_view_modal")
|
||||
},
|
||||
content: <QuickView />
|
||||
}
|
||||
]
|
||||
|
||||
// Handle ESC key to close modal
|
||||
useEffect(() => {
|
||||
for (const item of modalConfigs) {
|
||||
if (!item?.isOpen) {
|
||||
handleCloseModal(item?.id || "")
|
||||
}
|
||||
}
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
for (const item of modalConfigs) {
|
||||
handleCloseModal(item?.id || "")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleEscKey);
|
||||
return () => window.removeEventListener('keydown', handleEscKey);
|
||||
}, [isOpenRelic]);
|
||||
|
||||
return (
|
||||
<div className="max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
{/* Left Section - Items Grid */}
|
||||
<div className="lg:col-span-2">
|
||||
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
|
||||
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
|
||||
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
|
||||
{transI18n("relics")}
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
|
||||
{["1", "2", "3", "4", "5", "6"].map((item, index) => (
|
||||
<div key={index} className="relative group">
|
||||
<div
|
||||
onClick={() => {
|
||||
if (item === selectedRelicSlot) {
|
||||
setSelectedRelicSlot("")
|
||||
} else {
|
||||
setSelectedRelicSlot(item)
|
||||
}
|
||||
handlerChangeRelic(item)
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<RelicCard
|
||||
slot={item}
|
||||
avatarId={avatarSelected?.ID.toString() || ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handlerChangeRelic(item)
|
||||
}}
|
||||
className="btn btn-info p-1.5 rounded-full shadow-lg transition-colors duration-200"
|
||||
title={transI18n("changeRelic")}
|
||||
>
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{getRelic(item) && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${item}?`)) {
|
||||
handlerDeleteRelic(item)
|
||||
}
|
||||
}}
|
||||
className="btn btn-error p-1.5 rounded-full shadow-lg transition-colors duration-200"
|
||||
title={transI18n("deleteRelic")}
|
||||
>
|
||||
<svg className="w-3 h-3" 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>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 mt-10">
|
||||
<button
|
||||
disabled={!selectedRelicSlot}
|
||||
onClick={() => {
|
||||
handlerChangeRelic(selectedRelicSlot)
|
||||
}}
|
||||
className="btn btn-info"
|
||||
>
|
||||
{transI18n("changeRelic")}
|
||||
</button>
|
||||
<button
|
||||
disabled={!selectedRelicSlot}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${selectedRelicSlot}?`)) {
|
||||
handlerDeleteRelic(selectedRelicSlot)
|
||||
}
|
||||
}}
|
||||
className="btn btn-error"
|
||||
>
|
||||
{transI18n("deleteRelic")}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsOpenQuickView(true)
|
||||
handleShow("quick_view_modal")
|
||||
}}
|
||||
className="btn btn-info w-full mt-2"
|
||||
>
|
||||
{transI18n("quickView")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Section - Stats and Set Effects */}
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Set Effects Panel */}
|
||||
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
|
||||
<h3 className="flex items-center gap-2 text-xl font-bold mb-4 text-base-content">
|
||||
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
|
||||
{transI18n("setEffects")}
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
{relicEffects.map((setEffect, index) => {
|
||||
const relicInfo = mapRelicSet[setEffect.key];
|
||||
if (!relicInfo) return null;
|
||||
return (
|
||||
<div key={index} className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="font-bold text-warning"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, relicInfo.Name),
|
||||
[]
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{setEffect.count && (
|
||||
<span className={`text-sm text-info`}>
|
||||
({setEffect.count})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 pl-4">
|
||||
{Object.entries(relicInfo.Skills).map(([requireNum, value]) => {
|
||||
if (Number(requireNum) > Number(setEffect.count)) return null;
|
||||
return (
|
||||
<div key={requireNum} className="space-y-1">
|
||||
<div className={`font-medium text-success`}>
|
||||
{requireNum}-PC:
|
||||
</div>
|
||||
<div
|
||||
className="text-sm text-base-content/80 leading-relaxed pl-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, value.Desc),
|
||||
value.Param || []
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{modalConfigs.map(({ id, title, onClose, content }) => (
|
||||
<dialog key={id} id={id} className="modal">
|
||||
<div className="modal-box w-11/12 max-w-[90%] max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
|
||||
<div className="sticky top-0 z-10">
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.1, rotate: 90 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
|
||||
onClick={onClose}
|
||||
>
|
||||
✕
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
{title && (
|
||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
||||
{title}
|
||||
</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{content}
|
||||
</div>
|
||||
</dialog>
|
||||
))}
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,112 +1,112 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import dynamic from "next/dynamic"
|
||||
import type { SingleValue } from "react-select"
|
||||
import Image from "next/image"
|
||||
import useLocaleStore from "@/stores/localeStore"
|
||||
import ParseText from "../parseText"
|
||||
import { themeColors } from "@/constant/constant"
|
||||
import type { Props as SelectProps } from "react-select"
|
||||
import { JSX } from "react"
|
||||
|
||||
const Select = dynamic(
|
||||
() => import("react-select").then(m => m.default),
|
||||
{ ssr: false }
|
||||
) as <Option, IsMulti extends boolean = false>(
|
||||
props: SelectProps<Option, IsMulti>
|
||||
) => JSX.Element
|
||||
|
||||
export type SelectOption = {
|
||||
value: string
|
||||
label: string
|
||||
imageUrl: string
|
||||
}
|
||||
|
||||
type SelectCustomProp = {
|
||||
customSet: SelectOption[]
|
||||
excludeSet: SelectOption[]
|
||||
selectedCustomSet: string
|
||||
placeholder: string
|
||||
setSelectedCustomSet: (value: string) => void
|
||||
}
|
||||
|
||||
export default function SelectCustomImage({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
|
||||
const options: SelectOption[] = customSet
|
||||
const { locale, theme } = useLocaleStore()
|
||||
|
||||
const c = themeColors[theme] || themeColors.winter
|
||||
|
||||
const customStyles = {
|
||||
option: (p: any, s: any) => ({
|
||||
...p,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 12px',
|
||||
backgroundColor: s.isFocused ? c.bgHover : c.bg,
|
||||
color: c.text,
|
||||
cursor: 'pointer'
|
||||
}),
|
||||
|
||||
singleValue: (p: any) => ({
|
||||
...p,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
color: c.text
|
||||
}),
|
||||
|
||||
control: (p: any) => ({
|
||||
...p,
|
||||
backgroundColor: c.bg,
|
||||
borderColor: c.border,
|
||||
boxShadow: 'none'
|
||||
}),
|
||||
|
||||
menu: (p: any) => ({
|
||||
...p,
|
||||
backgroundColor: c.bg,
|
||||
color: c.text,
|
||||
zIndex: 9999
|
||||
}),
|
||||
|
||||
menuPortal: (p: any) => ({
|
||||
...p,
|
||||
zIndex: 9999
|
||||
})
|
||||
}
|
||||
|
||||
const formatOptionLabel = (option: SelectOption) => (
|
||||
<div className="flex items-center gap-1 w-full h-full">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={option.imageUrl}
|
||||
alt=""
|
||||
width={125}
|
||||
height={125}
|
||||
className="w-8 h-8 object-contain bg-warning-content rounded-full"
|
||||
/>
|
||||
<ParseText className='font-bold' text={option.label} locale={locale} />
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={options.filter(opt => !excludeSet.some(ex => ex.value === opt.value))}
|
||||
value={options.find(opt => {
|
||||
return opt.value === selectedCustomSet
|
||||
}) || null}
|
||||
onChange={(selected: SingleValue<SelectOption>) => {
|
||||
setSelectedCustomSet(selected?.value || '')
|
||||
}}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
styles={customStyles}
|
||||
placeholder={placeholder}
|
||||
className="my-react-select-container"
|
||||
classNamePrefix="my-react-select"
|
||||
isSearchable
|
||||
isClearable
|
||||
/>
|
||||
)
|
||||
}
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import dynamic from "next/dynamic"
|
||||
import type { SingleValue } from "react-select"
|
||||
import Image from "next/image"
|
||||
import useLocaleStore from "@/stores/localeStore"
|
||||
import ParseText from "../parseText"
|
||||
import { themeColors } from "@/constant/constant"
|
||||
import type { Props as SelectProps } from "react-select"
|
||||
import { JSX } from "react"
|
||||
|
||||
const Select = dynamic(
|
||||
() => import("react-select").then(m => m.default),
|
||||
{ ssr: false }
|
||||
) as <Option, IsMulti extends boolean = false>(
|
||||
props: SelectProps<Option, IsMulti>
|
||||
) => JSX.Element
|
||||
|
||||
export type SelectOption = {
|
||||
value: string
|
||||
label: string
|
||||
imageUrl: string
|
||||
}
|
||||
|
||||
type SelectCustomProp = {
|
||||
customSet: SelectOption[]
|
||||
excludeSet: SelectOption[]
|
||||
selectedCustomSet: string
|
||||
placeholder: string
|
||||
setSelectedCustomSet: (value: string) => void
|
||||
}
|
||||
|
||||
export default function SelectCustomImage({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
|
||||
const options: SelectOption[] = customSet
|
||||
const { locale, theme } = useLocaleStore()
|
||||
|
||||
const c = themeColors[theme] || themeColors.winter
|
||||
|
||||
const customStyles = {
|
||||
option: (p: any, s: any) => ({
|
||||
...p,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 12px',
|
||||
backgroundColor: s.isFocused ? c.bgHover : c.bg,
|
||||
color: c.text,
|
||||
cursor: 'pointer'
|
||||
}),
|
||||
|
||||
singleValue: (p: any) => ({
|
||||
...p,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
color: c.text
|
||||
}),
|
||||
|
||||
control: (p: any) => ({
|
||||
...p,
|
||||
backgroundColor: c.bg,
|
||||
borderColor: c.border,
|
||||
boxShadow: 'none'
|
||||
}),
|
||||
|
||||
menu: (p: any) => ({
|
||||
...p,
|
||||
backgroundColor: c.bg,
|
||||
color: c.text,
|
||||
zIndex: 9999
|
||||
}),
|
||||
|
||||
menuPortal: (p: any) => ({
|
||||
...p,
|
||||
zIndex: 9999
|
||||
})
|
||||
}
|
||||
|
||||
const formatOptionLabel = (option: SelectOption) => (
|
||||
<div className="flex items-center gap-1 w-full h-full">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={option.imageUrl}
|
||||
alt=""
|
||||
width={125}
|
||||
height={125}
|
||||
className="w-8 h-8 object-contain bg-warning-content rounded-full"
|
||||
/>
|
||||
<ParseText className='font-bold' text={option.label} locale={locale} />
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={options.filter(opt => !excludeSet.some(ex => ex.value === opt.value))}
|
||||
value={options.find(opt => {
|
||||
return opt.value === selectedCustomSet
|
||||
}) || null}
|
||||
onChange={(selected: SingleValue<SelectOption>) => {
|
||||
setSelectedCustomSet(selected?.value || '')
|
||||
}}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
styles={customStyles}
|
||||
placeholder={placeholder}
|
||||
className="my-react-select-container"
|
||||
classNamePrefix="my-react-select"
|
||||
isSearchable
|
||||
isClearable
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,118 +1,118 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import dynamic from "next/dynamic"
|
||||
import type { SingleValue } from "react-select"
|
||||
import { replaceByParam } from '@/helper'
|
||||
import useLocaleStore from '@/stores/localeStore'
|
||||
import { themeColors } from '@/constant/constant'
|
||||
import type { Props as SelectProps } from "react-select"
|
||||
import { JSX } from "react"
|
||||
|
||||
const Select = dynamic(
|
||||
() => import("react-select").then(m => m.default),
|
||||
{ ssr: false }
|
||||
) as <Option, IsMulti extends boolean = false>(
|
||||
props: SelectProps<Option, IsMulti>
|
||||
) => JSX.Element
|
||||
|
||||
export type SelectOption = {
|
||||
id: string
|
||||
name: string
|
||||
time?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
type SelectCustomProp = {
|
||||
customSet: SelectOption[]
|
||||
excludeSet: SelectOption[]
|
||||
selectedCustomSet: string
|
||||
placeholder: string
|
||||
setSelectedCustomSet: (value: string) => void
|
||||
}
|
||||
|
||||
export default function SelectCustomText({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
|
||||
const options: SelectOption[] = customSet
|
||||
const { theme } = useLocaleStore()
|
||||
const c = themeColors[theme] || themeColors.winter
|
||||
|
||||
const customStyles = {
|
||||
option: (p: any, s: any) => ({
|
||||
...p,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 12px',
|
||||
backgroundColor: s.isFocused ? c.bgHover : c.bg,
|
||||
color: c.text,
|
||||
cursor: 'pointer'
|
||||
}),
|
||||
|
||||
singleValue: (p: any) => ({
|
||||
...p,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
color: c.text
|
||||
}),
|
||||
|
||||
control: (p: any) => ({
|
||||
...p,
|
||||
backgroundColor: c.bg,
|
||||
borderColor: c.border,
|
||||
boxShadow: 'none'
|
||||
}),
|
||||
|
||||
menu: (p: any) => ({
|
||||
...p,
|
||||
backgroundColor: c.bg,
|
||||
color: c.text,
|
||||
zIndex: 9999
|
||||
}),
|
||||
|
||||
menuPortal: (p: any) => ({
|
||||
...p,
|
||||
zIndex: 9999
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const formatOptionLabel = (option: SelectOption) => (
|
||||
<div className="flex flex-col gap-1 w-full h-full">
|
||||
<div
|
||||
className="font-bold text-lg"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
option.name,
|
||||
[]
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{option.time && <div className='text-base'>{option.time}</div>}
|
||||
{option.description && <div
|
||||
className="text-base"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: option.description
|
||||
}}
|
||||
/>}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={options.filter(opt => !excludeSet.some(ex => ex.id === opt.id))}
|
||||
value={options.find(opt => {
|
||||
return opt.id === selectedCustomSet
|
||||
}) || null}
|
||||
onChange={(selected: SingleValue<SelectOption>) => {
|
||||
setSelectedCustomSet(selected?.id || '')
|
||||
}}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
styles={customStyles}
|
||||
placeholder={placeholder}
|
||||
className="my-react-select-container"
|
||||
classNamePrefix="my-react-select"
|
||||
isSearchable
|
||||
isClearable
|
||||
/>
|
||||
)
|
||||
}
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
'use client'
|
||||
import dynamic from "next/dynamic"
|
||||
import type { SingleValue } from "react-select"
|
||||
import { replaceByParam } from '@/helper'
|
||||
import useLocaleStore from '@/stores/localeStore'
|
||||
import { themeColors } from '@/constant/constant'
|
||||
import type { Props as SelectProps } from "react-select"
|
||||
import { JSX } from "react"
|
||||
|
||||
const Select = dynamic(
|
||||
() => import("react-select").then(m => m.default),
|
||||
{ ssr: false }
|
||||
) as <Option, IsMulti extends boolean = false>(
|
||||
props: SelectProps<Option, IsMulti>
|
||||
) => JSX.Element
|
||||
|
||||
export type SelectOption = {
|
||||
id: string
|
||||
name: string
|
||||
time?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
type SelectCustomProp = {
|
||||
customSet: SelectOption[]
|
||||
excludeSet: SelectOption[]
|
||||
selectedCustomSet: string
|
||||
placeholder: string
|
||||
setSelectedCustomSet: (value: string) => void
|
||||
}
|
||||
|
||||
export default function SelectCustomText({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
|
||||
const options: SelectOption[] = customSet
|
||||
const { theme } = useLocaleStore()
|
||||
const c = themeColors[theme] || themeColors.winter
|
||||
|
||||
const customStyles = {
|
||||
option: (p: any, s: any) => ({
|
||||
...p,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 12px',
|
||||
backgroundColor: s.isFocused ? c.bgHover : c.bg,
|
||||
color: c.text,
|
||||
cursor: 'pointer'
|
||||
}),
|
||||
|
||||
singleValue: (p: any) => ({
|
||||
...p,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
color: c.text
|
||||
}),
|
||||
|
||||
control: (p: any) => ({
|
||||
...p,
|
||||
backgroundColor: c.bg,
|
||||
borderColor: c.border,
|
||||
boxShadow: 'none'
|
||||
}),
|
||||
|
||||
menu: (p: any) => ({
|
||||
...p,
|
||||
backgroundColor: c.bg,
|
||||
color: c.text,
|
||||
zIndex: 9999
|
||||
}),
|
||||
|
||||
menuPortal: (p: any) => ({
|
||||
...p,
|
||||
zIndex: 9999
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const formatOptionLabel = (option: SelectOption) => (
|
||||
<div className="flex flex-col gap-1 w-full h-full">
|
||||
<div
|
||||
className="font-bold text-lg"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
option.name,
|
||||
[]
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{option.time && <div className='text-base'>{option.time}</div>}
|
||||
{option.description && <div
|
||||
className="text-base"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: option.description
|
||||
}}
|
||||
/>}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<Select
|
||||
options={options.filter(opt => !excludeSet.some(ex => ex.id === opt.id))}
|
||||
value={options.find(opt => {
|
||||
return opt.id === selectedCustomSet
|
||||
}) || null}
|
||||
onChange={(selected: SingleValue<SelectOption>) => {
|
||||
setSelectedCustomSet(selected?.id || '')
|
||||
}}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
styles={customStyles}
|
||||
placeholder={placeholder}
|
||||
className="my-react-select-container"
|
||||
classNamePrefix="my-react-select"
|
||||
isSearchable
|
||||
isClearable
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
+1006
-1006
File diff suppressed because it is too large
Load Diff
@@ -1,108 +1,108 @@
|
||||
|
||||
"use client"
|
||||
|
||||
import NextImage from "next/image"
|
||||
import { AvatarDetail, RelicShowcaseType } from "@/types";
|
||||
|
||||
export default function RelicShowcase({
|
||||
relic,
|
||||
avatarInfo,
|
||||
}: {
|
||||
relic: RelicShowcaseType;
|
||||
avatarInfo: AvatarDetail;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="relative w-full flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600/60 bg-linear-to-r from-yellow-600/20 to-transparent"
|
||||
>
|
||||
{/* Subtle glow overlay */}
|
||||
<div className="absolute inset-0 rounded-s-lg pointer-events-none"></div>
|
||||
|
||||
<div className="flex relative">
|
||||
<div className="absolute inset-0 rounded-lg blur-lg -z-10"></div>
|
||||
<NextImage
|
||||
src={relic?.img || ""}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
width={78}
|
||||
height={78}
|
||||
alt="Relic Icon"
|
||||
className="h-19.5 w-19.5 rounded-lg"
|
||||
/>
|
||||
|
||||
<div
|
||||
className="absolute text-yellow-400 font-bold z-10 drop-shadow-[0_0_6px_rgba(251,191,36,0.8)]"
|
||||
style={{
|
||||
left: '0.65rem',
|
||||
bottom: '-0.45rem',
|
||||
fontSize: '1.05rem',
|
||||
letterSpacing: '-0.1em',
|
||||
}}
|
||||
>
|
||||
✦✦✦✦✦
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className=" flex w-1/6 flex-col items-center justify-center">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-yellow-500/15 rounded-full blur-md -z-10"></div>
|
||||
<NextImage
|
||||
src={`${process.env.CDN_URL}/${relic?.mainAffix?.detail?.icon}` || ""}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
width={35}
|
||||
height={35}
|
||||
alt="Main Affix Icon"
|
||||
className="h-8.75 w-8.75"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-base text-yellow-400 font-semibold drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]">
|
||||
{relic?.mainAffix?.valueAffix + relic?.mainAffix?.detail?.unit}
|
||||
</span>
|
||||
<span className="bg-black/20 backdrop-blur-sm rounded px-1.5 py-0.5 text-xs border border-white/10">
|
||||
+{relic?.mainAffix?.level}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ opacity: 0.3, height: '78px', borderLeftWidth: '1px' }}></div>
|
||||
|
||||
<div className="grid w-[65%] m-2 grid-cols-2 gap-1">
|
||||
{relic?.subAffix?.map((subAffix, index) => {
|
||||
if (!subAffix) return null
|
||||
return (
|
||||
<div key={index} className="flex flex-col">
|
||||
<div className="relative flex flex-row items-center bg-black/20 backdrop-blur-sm rounded-md p-1 border border-white/5 min-w-0">
|
||||
{subAffix?.detail?.icon ? (
|
||||
<NextImage
|
||||
src={`${process.env.CDN_URL}/${subAffix?.detail?.icon}` || ""}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
width={32}
|
||||
height={32}
|
||||
alt="Sub Affix Icon"
|
||||
className="h-6 w-6 shrink-0"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-6 w-6 bg-black/60 rounded flex items-center justify-center border border-white/10 shrink-0">
|
||||
<span className="text-xs text-white/50">?</span>
|
||||
</div>
|
||||
)}
|
||||
<span className="text-xs text-gray-200 ml-0.5 truncate flex-1 min-w-0">
|
||||
+{subAffix?.valueAffix + subAffix?.detail?.unit}
|
||||
</span>
|
||||
{
|
||||
(avatarInfo?.Relics?.SubAffixPropertyList.findIndex((item) => item === subAffix?.property) !== -1) && (
|
||||
<span className="ml-1 bg-yellow-600/20 text-yellow-400 rounded-full px-1 py-0.5 text-[10px] font-semibold border border-yellow-600/30 shrink-0 leading-none">
|
||||
{subAffix?.count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
"use client"
|
||||
|
||||
import NextImage from "next/image"
|
||||
import { AvatarDetail, RelicShowcaseType } from "@/types";
|
||||
|
||||
export default function RelicShowcase({
|
||||
relic,
|
||||
avatarInfo,
|
||||
}: {
|
||||
relic: RelicShowcaseType;
|
||||
avatarInfo: AvatarDetail;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="relative w-full flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600/60 bg-linear-to-r from-yellow-600/20 to-transparent"
|
||||
>
|
||||
{/* Subtle glow overlay */}
|
||||
<div className="absolute inset-0 rounded-s-lg pointer-events-none"></div>
|
||||
|
||||
<div className="flex relative">
|
||||
<div className="absolute inset-0 rounded-lg blur-lg -z-10"></div>
|
||||
<NextImage
|
||||
src={relic?.img || ""}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
width={78}
|
||||
height={78}
|
||||
alt="Relic Icon"
|
||||
className="h-19.5 w-19.5 rounded-lg"
|
||||
/>
|
||||
|
||||
<div
|
||||
className="absolute text-yellow-400 font-bold z-10 drop-shadow-[0_0_6px_rgba(251,191,36,0.8)]"
|
||||
style={{
|
||||
left: '0.65rem',
|
||||
bottom: '-0.45rem',
|
||||
fontSize: '1.05rem',
|
||||
letterSpacing: '-0.1em',
|
||||
}}
|
||||
>
|
||||
✦✦✦✦✦
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className=" flex w-1/6 flex-col items-center justify-center">
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 bg-yellow-500/15 rounded-full blur-md -z-10"></div>
|
||||
<NextImage
|
||||
src={`${process.env.CDN_URL}/${relic?.mainAffix?.detail?.icon}` || ""}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
width={35}
|
||||
height={35}
|
||||
alt="Main Affix Icon"
|
||||
className="h-8.75 w-8.75"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-base text-yellow-400 font-semibold drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]">
|
||||
{relic?.mainAffix?.valueAffix + relic?.mainAffix?.detail?.unit}
|
||||
</span>
|
||||
<span className="bg-black/20 backdrop-blur-sm rounded px-1.5 py-0.5 text-xs border border-white/10">
|
||||
+{relic?.mainAffix?.level}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ opacity: 0.3, height: '78px', borderLeftWidth: '1px' }}></div>
|
||||
|
||||
<div className="grid w-[65%] m-2 grid-cols-2 gap-1">
|
||||
{relic?.subAffix?.map((subAffix, index) => {
|
||||
if (!subAffix) return null
|
||||
return (
|
||||
<div key={index} className="flex flex-col">
|
||||
<div className="relative flex flex-row items-center bg-black/20 backdrop-blur-sm rounded-md p-1 border border-white/5 min-w-0">
|
||||
{subAffix?.detail?.icon ? (
|
||||
<NextImage
|
||||
src={`${process.env.CDN_URL}/${subAffix?.detail?.icon}` || ""}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
width={32}
|
||||
height={32}
|
||||
alt="Sub Affix Icon"
|
||||
className="h-6 w-6 shrink-0"
|
||||
/>
|
||||
) : (
|
||||
<div className="h-6 w-6 bg-black/60 rounded flex items-center justify-center border border-white/10 shrink-0">
|
||||
<span className="text-xs text-white/50">?</span>
|
||||
</div>
|
||||
)}
|
||||
<span className="text-xs text-gray-200 ml-0.5 truncate flex-1 min-w-0">
|
||||
+{subAffix?.valueAffix + subAffix?.detail?.unit}
|
||||
</span>
|
||||
{
|
||||
(avatarInfo?.Relics?.SubAffixPropertyList.findIndex((item) => item === subAffix?.property) !== -1) && (
|
||||
<span className="ml-1 bg-yellow-600/20 text-yellow-400 rounded-full px-1 py-0.5 text-[10px] font-semibold border border-yellow-600/30 shrink-0 leading-none">
|
||||
{subAffix?.count}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+384
-384
@@ -1,385 +1,385 @@
|
||||
"use client"
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useMemo } from "react";
|
||||
import { traceButtonsInfo, traceLink } from "@/constant/traceConstant";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import Image from "next/image";
|
||||
import { replaceByParam, getLocaleName } from '@/helper';
|
||||
import { mappingStats } from "@/constant/constant";
|
||||
import { toast } from "react-toastify";
|
||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||
import { StatusAdd } from '@/types/avatarDetail';
|
||||
import { SkillDescription } from "./skillDescription";
|
||||
|
||||
export default function SkillsInfo() {
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { theme } = useLocaleStore()
|
||||
const { avatarSelected, skillIDSelected, setSkillIDSelected } = useCurrentDataStore()
|
||||
const { avatars, setAvatar } = useUserDataStore()
|
||||
const { locale } = useLocaleStore()
|
||||
const traceButtons = useMemo(() => {
|
||||
if (!avatarSelected) return
|
||||
return traceButtonsInfo[avatarSelected.BaseType]
|
||||
}, [avatarSelected])
|
||||
|
||||
const avatarData = useMemo(() => {
|
||||
if (!avatarSelected) return
|
||||
return avatars[avatarSelected?.ID?.toString()]
|
||||
}, [avatarSelected, avatars])
|
||||
|
||||
const avatarSkillTree = useMemo(() => {
|
||||
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
|
||||
if (avatars[avatarSelected?.ID?.toString()].enhanced) {
|
||||
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
|
||||
}
|
||||
return avatarSelected?.SkillTrees || {}
|
||||
}, [avatarSelected, avatars])
|
||||
|
||||
const skillInfo = useMemo(() => {
|
||||
if (!avatarSelected || !skillIDSelected) return
|
||||
return avatarSkillTree?.[skillIDSelected || ""]?.["1"]
|
||||
}, [avatarSelected, avatarSkillTree, skillIDSelected])
|
||||
|
||||
const getTraceBuffDisplay = (status: StatusAdd) => {
|
||||
const dataDisplay = mappingStats[status.PropertyType]
|
||||
if (!dataDisplay) return ""
|
||||
if (dataDisplay.unit === "%") {
|
||||
return `${(status.Value * 100).toFixed(1)}${dataDisplay.unit}`
|
||||
}
|
||||
if (dataDisplay.name === "SPD") {
|
||||
return `${status.Value.toFixed(1)}${dataDisplay.unit}`
|
||||
}
|
||||
return `${status.Value.toFixed(0)}${dataDisplay.unit}`
|
||||
}
|
||||
|
||||
const dataLevelUpSkill = useMemo(() => {
|
||||
const skillIds: number[] = skillInfo?.LevelUpSkillID || []
|
||||
if (!avatarSelected || !avatarData) return undefined
|
||||
let result = Object.values(avatarSelected.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
|
||||
if (avatarData.enhanced) {
|
||||
result = Object.values(avatarSelected?.Enhanced?.[avatarData.enhanced.toString()]?.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
|
||||
}
|
||||
|
||||
if (result && result.length > 0) {
|
||||
return {
|
||||
isServant: false,
|
||||
data: result,
|
||||
servantData: null,
|
||||
}
|
||||
}
|
||||
const resultServant = Object.values(avatarSelected?.Memosprite?.Skills || {})
|
||||
?.filter((skill) => skillIds.includes(skill.ID))
|
||||
|
||||
if (resultServant && resultServant.length > 0) {
|
||||
return {
|
||||
isServant: true,
|
||||
data: resultServant,
|
||||
servantData: avatarSelected.Memosprite,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}, [skillInfo?.LevelUpSkillID, avatarSelected, avatarData])
|
||||
|
||||
|
||||
const handlerMaxAll = () => {
|
||||
if (!avatarData || !avatarSkillTree) {
|
||||
toast.error(transI18n("maxAllFailed"))
|
||||
return
|
||||
}
|
||||
const newData = structuredClone(avatarData)
|
||||
newData.data.skills = Object.values(avatarSkillTree).reduce((acc, dataPointEntry) => {
|
||||
const firstEntry = Object.values(dataPointEntry)[0];
|
||||
if (firstEntry) {
|
||||
acc[firstEntry.PointID] = firstEntry.MaxLevel;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, number>)
|
||||
toast.success(transI18n("maxAllSuccess"))
|
||||
setAvatar(newData)
|
||||
}
|
||||
|
||||
const handlerChangeStatusTrace = (status: boolean) => {
|
||||
if (!avatarData || !skillInfo) return
|
||||
const newData = structuredClone(avatarData)
|
||||
newData.data.skills[skillInfo?.PointID] = status ? 1 : 0
|
||||
|
||||
if (!status && traceLink?.[avatarSelected?.BaseType || ""]?.[skillIDSelected || ""]) {
|
||||
traceLink[avatarSelected?.BaseType || ""][skillIDSelected || ""].forEach((pointId) => {
|
||||
if (avatarSkillTree?.[pointId]?.["1"]) {
|
||||
newData.data.skills[avatarSkillTree?.[pointId]?.["1"].PointID] = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
setAvatar(newData)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div className="rounded-xl p-6 shadow-lg">
|
||||
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
|
||||
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
|
||||
{transI18n("skills")}
|
||||
</h2>
|
||||
<div className="flex flex-col items-center">
|
||||
<button className="btn btn-success" onClick={handlerMaxAll}>{transI18n("maxAll")}</button>
|
||||
{traceButtons && avatarSelected && (
|
||||
<div className="grid col-span-4 relative w-full aspect-square">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`/skilltree/${avatarSelected?.BaseType?.toUpperCase()}.webp`}
|
||||
alt=""
|
||||
width={312}
|
||||
priority={true}
|
||||
height={312}
|
||||
style={{
|
||||
filter: (theme === "winter" || theme === "cupcake") ? "invert(1)" : "none"
|
||||
}}
|
||||
className={`w-full h-full object-cover rounded-xl`}
|
||||
/>
|
||||
{traceButtons.map((btn, index) => {
|
||||
if (!avatarSelected?.SkillTrees?.[btn.id]) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={`${btn.id} + ${index}`}
|
||||
id={btn.id}
|
||||
className={`
|
||||
absolute rounded-full border border-black
|
||||
bg-no-repeat bg-contain
|
||||
cursor-pointer transition-all duration-200 ease-in-out
|
||||
shadow-[0_0_5px_white] flex justify-center items-center
|
||||
hover:scale-110z-10
|
||||
${btn.size === "small" ? "w-[6vw] h-[6vw] md:w-[2vw] md:h-[2vw] bg-white" : ""}
|
||||
${btn.size === "medium" ? "w-[8vw] h-[8vw] md:w-[3vw] md:h-[3vw] bg-white" : ""}
|
||||
${btn.size === "big" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
|
||||
${btn.size === "special" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-white" : ""}
|
||||
${btn.size === "memory" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
|
||||
${btn.size === "elation" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
|
||||
${skillIDSelected === btn.id ? "border-4 border-primary" : ""}
|
||||
${!avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID]
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: ""}
|
||||
`}
|
||||
onClick={() => {
|
||||
setSkillIDSelected(btn.id === skillIDSelected ? null : btn.id)
|
||||
}}
|
||||
style={{
|
||||
left: btn.left,
|
||||
top: btn.top,
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/${avatarSelected?.SkillTrees?.[btn.id]?.["1"]?.Icon}`}
|
||||
alt={btn.id.replaceAll("Point", "")}
|
||||
priority={true}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
width={124}
|
||||
height={124}
|
||||
style={{
|
||||
objectFit: "contain",
|
||||
padding: "2px",
|
||||
filter: (btn.size !== "big" && btn.size !== "memory" && btn.size !== "elation") ? "brightness(0%)" : ""
|
||||
}}
|
||||
/>
|
||||
{(btn.size === "big" || btn.size === "memory" || btn.size === "elation") && (
|
||||
<p className="
|
||||
z-12 text-sm sm:text-xs lg:text-sm xl:text-base 2xl:text-2xl
|
||||
font-bold text-center rounded-full absolute
|
||||
translate-y-full mt-1
|
||||
bg-base-300 px-1
|
||||
left-1/2 transform -translate-x-1/2
|
||||
">
|
||||
{`${avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] || 0}/${avatarSkillTree?.[btn.id]?.["1"]?.MaxLevel}`}
|
||||
</p>
|
||||
|
||||
)}
|
||||
{btn.size === "special" && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
backgroundColor: "#4a6eff",
|
||||
mixBlendMode: "screen",
|
||||
opacity: 0.8,
|
||||
borderRadius: "50%"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{btn.size === "memory" && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
backgroundColor: "#9a89ff",
|
||||
mixBlendMode: "screen",
|
||||
opacity: 0.4,
|
||||
borderRadius: "50%"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{btn.size === "elation" && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
backgroundColor: "#ff8c00",
|
||||
mixBlendMode: "screen",
|
||||
opacity: 0.5,
|
||||
borderRadius: "50%"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{btn.size === "big" && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
backgroundColor: "#f5e4b0",
|
||||
mixBlendMode: "screen",
|
||||
opacity: 0.3,
|
||||
borderRadius: "50%"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!traceButtons && avatarSelected && (
|
||||
<div className="flex flex-col relative w-full aspect-square">
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
|
||||
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
|
||||
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
|
||||
{transI18n("details")}
|
||||
</h2>
|
||||
{skillIDSelected && avatarSelected?.SkillTrees && avatarData && (
|
||||
<div>
|
||||
{skillInfo?.MaxLevel && skillInfo?.MaxLevel > 1 ? (
|
||||
<div>
|
||||
<div className="font-bold text-success">{transI18n("level")}</div>
|
||||
<div className="w-full max-w-xs">
|
||||
<input type="range"
|
||||
min={1}
|
||||
max={skillInfo?.MaxLevel || 1}
|
||||
value={avatarData?.data.skills?.[skillInfo?.PointID] || 1}
|
||||
onChange={(e) => {
|
||||
const newData = structuredClone(avatarData)
|
||||
newData.data.skills[skillInfo?.PointID] = parseInt(e.target.value)
|
||||
setAvatar(newData)
|
||||
}}
|
||||
className="range range-success"
|
||||
step="1" />
|
||||
<div className="flex justify-between px-2.5 mt-2 text-xs">
|
||||
{Array.from({ length: skillInfo?.MaxLevel }, (_, index) => index + 1).map((index) => (
|
||||
<span key={index}>{index}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : skillInfo?.MaxLevel && skillInfo?.MaxLevel === 1 && traceButtons?.find((btn) => btn.id === skillIDSelected)?.size !== "big" ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={avatarData?.data.skills?.[skillInfo?.PointID] === 1}
|
||||
className="toggle toggle-success"
|
||||
onChange={(e) => {
|
||||
if (traceButtons?.find((btn) => btn.id === skillIDSelected)?.size === "special") {
|
||||
if (e.target.checked) {
|
||||
const newData = structuredClone(avatarData)
|
||||
newData.data.skills[skillInfo?.PointID] = 1
|
||||
setAvatar(newData)
|
||||
return
|
||||
}
|
||||
const newData = structuredClone(avatarData)
|
||||
delete newData.data.skills[skillInfo?.PointID]
|
||||
setAvatar(newData)
|
||||
return
|
||||
}
|
||||
handlerChangeStatusTrace(e.target.checked)
|
||||
}}
|
||||
/>
|
||||
<div className="font-bold text-success">
|
||||
{avatarData?.data.skills?.[skillInfo?.PointID] === 1 ? transI18n("active") : transI18n("inactive")}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
null
|
||||
)}
|
||||
|
||||
{((skillInfo?.PointName && skillInfo?.PointDesc) ||
|
||||
(skillInfo?.PointName && skillInfo?.StatusAddList.length > 0))
|
||||
&& (
|
||||
<div className="text-xl font-bold flex items-center gap-2 mt-2">
|
||||
{getLocaleName(locale, skillInfo.PointName)}
|
||||
{skillInfo.StatusAddList.length > 0 && (
|
||||
<div>
|
||||
{skillInfo.StatusAddList.map((status, index) => (
|
||||
<div key={index}>
|
||||
<div className="text-xl font-bold">{getTraceBuffDisplay(status)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{skillInfo?.PointDesc && (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, skillInfo?.PointDesc) || "",
|
||||
skillInfo?.Param || []
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{skillInfo?.LevelUpSkillID
|
||||
&& skillInfo?.LevelUpSkillID.length > 0
|
||||
&& dataLevelUpSkill
|
||||
&& (
|
||||
<div className="mt-2 flex flex-col gap-2">
|
||||
|
||||
{dataLevelUpSkill?.data?.map((skill, index) => (
|
||||
<div key={index}>
|
||||
|
||||
<div className="text-xl font-bold text-primary">
|
||||
{transI18n(dataLevelUpSkill.isServant ? `${skill?.AttackType ? "severaltalent" : "servantskill"}` : `${skill?.AttackType ? skill?.AttackType.toLowerCase() : "talent"}`)}
|
||||
{` (${transI18n(skill?.SkillEffect?.toLowerCase())})`}
|
||||
</div>
|
||||
|
||||
<SkillDescription
|
||||
skill={skill}
|
||||
locale={locale}
|
||||
avatarData={avatarData}
|
||||
skillInfo={skillInfo}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
"use client"
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useMemo } from "react";
|
||||
import { traceButtonsInfo, traceLink } from "@/constant/traceConstant";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import Image from "next/image";
|
||||
import { replaceByParam, getLocaleName } from '@/helper';
|
||||
import { mappingStats } from "@/constant/constant";
|
||||
import { toast } from "react-toastify";
|
||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||
import { StatusAdd } from '@/types/avatarDetail';
|
||||
import { SkillDescription } from "./skillDescription";
|
||||
|
||||
export default function SkillsInfo() {
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { theme } = useLocaleStore()
|
||||
const { avatarSelected, skillIDSelected, setSkillIDSelected } = useCurrentDataStore()
|
||||
const { avatars, setAvatar } = useUserDataStore()
|
||||
const { locale } = useLocaleStore()
|
||||
const traceButtons = useMemo(() => {
|
||||
if (!avatarSelected) return
|
||||
return traceButtonsInfo[avatarSelected.BaseType]
|
||||
}, [avatarSelected])
|
||||
|
||||
const avatarData = useMemo(() => {
|
||||
if (!avatarSelected) return
|
||||
return avatars[avatarSelected?.ID?.toString()]
|
||||
}, [avatarSelected, avatars])
|
||||
|
||||
const avatarSkillTree = useMemo(() => {
|
||||
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
|
||||
if (avatars[avatarSelected?.ID?.toString()].enhanced) {
|
||||
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
|
||||
}
|
||||
return avatarSelected?.SkillTrees || {}
|
||||
}, [avatarSelected, avatars])
|
||||
|
||||
const skillInfo = useMemo(() => {
|
||||
if (!avatarSelected || !skillIDSelected) return
|
||||
return avatarSkillTree?.[skillIDSelected || ""]?.["1"]
|
||||
}, [avatarSelected, avatarSkillTree, skillIDSelected])
|
||||
|
||||
const getTraceBuffDisplay = (status: StatusAdd) => {
|
||||
const dataDisplay = mappingStats[status.PropertyType]
|
||||
if (!dataDisplay) return ""
|
||||
if (dataDisplay.unit === "%") {
|
||||
return `${(status.Value * 100).toFixed(1)}${dataDisplay.unit}`
|
||||
}
|
||||
if (dataDisplay.name === "SPD") {
|
||||
return `${status.Value.toFixed(1)}${dataDisplay.unit}`
|
||||
}
|
||||
return `${status.Value.toFixed(0)}${dataDisplay.unit}`
|
||||
}
|
||||
|
||||
const dataLevelUpSkill = useMemo(() => {
|
||||
const skillIds: number[] = skillInfo?.LevelUpSkillID || []
|
||||
if (!avatarSelected || !avatarData) return undefined
|
||||
let result = Object.values(avatarSelected.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
|
||||
if (avatarData.enhanced) {
|
||||
result = Object.values(avatarSelected?.Enhanced?.[avatarData.enhanced.toString()]?.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
|
||||
}
|
||||
|
||||
if (result && result.length > 0) {
|
||||
return {
|
||||
isServant: false,
|
||||
data: result,
|
||||
servantData: null,
|
||||
}
|
||||
}
|
||||
const resultServant = Object.values(avatarSelected?.Memosprite?.Skills || {})
|
||||
?.filter((skill) => skillIds.includes(skill.ID))
|
||||
|
||||
if (resultServant && resultServant.length > 0) {
|
||||
return {
|
||||
isServant: true,
|
||||
data: resultServant,
|
||||
servantData: avatarSelected.Memosprite,
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}, [skillInfo?.LevelUpSkillID, avatarSelected, avatarData])
|
||||
|
||||
|
||||
const handlerMaxAll = () => {
|
||||
if (!avatarData || !avatarSkillTree) {
|
||||
toast.error(transI18n("maxAllFailed"))
|
||||
return
|
||||
}
|
||||
const newData = structuredClone(avatarData)
|
||||
newData.data.skills = Object.values(avatarSkillTree).reduce((acc, dataPointEntry) => {
|
||||
const firstEntry = Object.values(dataPointEntry)[0];
|
||||
if (firstEntry) {
|
||||
acc[firstEntry.PointID] = firstEntry.MaxLevel;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, number>)
|
||||
toast.success(transI18n("maxAllSuccess"))
|
||||
setAvatar(newData)
|
||||
}
|
||||
|
||||
const handlerChangeStatusTrace = (status: boolean) => {
|
||||
if (!avatarData || !skillInfo) return
|
||||
const newData = structuredClone(avatarData)
|
||||
newData.data.skills[skillInfo?.PointID] = status ? 1 : 0
|
||||
|
||||
if (!status && traceLink?.[avatarSelected?.BaseType || ""]?.[skillIDSelected || ""]) {
|
||||
traceLink[avatarSelected?.BaseType || ""][skillIDSelected || ""].forEach((pointId) => {
|
||||
if (avatarSkillTree?.[pointId]?.["1"]) {
|
||||
newData.data.skills[avatarSkillTree?.[pointId]?.["1"].PointID] = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
setAvatar(newData)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div className="rounded-xl p-6 shadow-lg">
|
||||
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
|
||||
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
|
||||
{transI18n("skills")}
|
||||
</h2>
|
||||
<div className="flex flex-col items-center">
|
||||
<button className="btn btn-success" onClick={handlerMaxAll}>{transI18n("maxAll")}</button>
|
||||
{traceButtons && avatarSelected && (
|
||||
<div className="grid col-span-4 relative w-full aspect-square">
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`/skilltree/${avatarSelected?.BaseType?.toUpperCase()}.webp`}
|
||||
alt=""
|
||||
width={312}
|
||||
priority={true}
|
||||
height={312}
|
||||
style={{
|
||||
filter: (theme === "winter" || theme === "cupcake") ? "invert(1)" : "none"
|
||||
}}
|
||||
className={`w-full h-full object-cover rounded-xl`}
|
||||
/>
|
||||
{traceButtons.map((btn, index) => {
|
||||
if (!avatarSelected?.SkillTrees?.[btn.id]) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={`${btn.id} + ${index}`}
|
||||
id={btn.id}
|
||||
className={`
|
||||
absolute rounded-full border border-black
|
||||
bg-no-repeat bg-contain
|
||||
cursor-pointer transition-all duration-200 ease-in-out
|
||||
shadow-[0_0_5px_white] flex justify-center items-center
|
||||
hover:scale-110z-10
|
||||
${btn.size === "small" ? "w-[6vw] h-[6vw] md:w-[2vw] md:h-[2vw] bg-white" : ""}
|
||||
${btn.size === "medium" ? "w-[8vw] h-[8vw] md:w-[3vw] md:h-[3vw] bg-white" : ""}
|
||||
${btn.size === "big" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
|
||||
${btn.size === "special" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-white" : ""}
|
||||
${btn.size === "memory" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
|
||||
${btn.size === "elation" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
|
||||
${skillIDSelected === btn.id ? "border-4 border-primary" : ""}
|
||||
${!avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID]
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: ""}
|
||||
`}
|
||||
onClick={() => {
|
||||
setSkillIDSelected(btn.id === skillIDSelected ? null : btn.id)
|
||||
}}
|
||||
style={{
|
||||
left: btn.left,
|
||||
top: btn.top,
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/${avatarSelected?.SkillTrees?.[btn.id]?.["1"]?.Icon}`}
|
||||
alt={btn.id.replaceAll("Point", "")}
|
||||
priority={true}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
width={124}
|
||||
height={124}
|
||||
style={{
|
||||
objectFit: "contain",
|
||||
padding: "2px",
|
||||
filter: (btn.size !== "big" && btn.size !== "memory" && btn.size !== "elation") ? "brightness(0%)" : ""
|
||||
}}
|
||||
/>
|
||||
{(btn.size === "big" || btn.size === "memory" || btn.size === "elation") && (
|
||||
<p className="
|
||||
z-12 text-sm sm:text-xs lg:text-sm xl:text-base 2xl:text-2xl
|
||||
font-bold text-center rounded-full absolute
|
||||
translate-y-full mt-1
|
||||
bg-base-300 px-1
|
||||
left-1/2 transform -translate-x-1/2
|
||||
">
|
||||
{`${avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] || 0}/${avatarSkillTree?.[btn.id]?.["1"]?.MaxLevel}`}
|
||||
</p>
|
||||
|
||||
)}
|
||||
{btn.size === "special" && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
backgroundColor: "#4a6eff",
|
||||
mixBlendMode: "screen",
|
||||
opacity: 0.8,
|
||||
borderRadius: "50%"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{btn.size === "memory" && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
backgroundColor: "#9a89ff",
|
||||
mixBlendMode: "screen",
|
||||
opacity: 0.4,
|
||||
borderRadius: "50%"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{btn.size === "elation" && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
backgroundColor: "#ff8c00",
|
||||
mixBlendMode: "screen",
|
||||
opacity: 0.5,
|
||||
borderRadius: "50%"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{btn.size === "big" && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
backgroundColor: "#f5e4b0",
|
||||
mixBlendMode: "screen",
|
||||
opacity: 0.3,
|
||||
borderRadius: "50%"
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!traceButtons && avatarSelected && (
|
||||
<div className="flex flex-col relative w-full aspect-square">
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
|
||||
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
|
||||
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
|
||||
{transI18n("details")}
|
||||
</h2>
|
||||
{skillIDSelected && avatarSelected?.SkillTrees && avatarData && (
|
||||
<div>
|
||||
{skillInfo?.MaxLevel && skillInfo?.MaxLevel > 1 ? (
|
||||
<div>
|
||||
<div className="font-bold text-success">{transI18n("level")}</div>
|
||||
<div className="w-full max-w-xs">
|
||||
<input type="range"
|
||||
min={1}
|
||||
max={skillInfo?.MaxLevel || 1}
|
||||
value={avatarData?.data.skills?.[skillInfo?.PointID] || 1}
|
||||
onChange={(e) => {
|
||||
const newData = structuredClone(avatarData)
|
||||
newData.data.skills[skillInfo?.PointID] = parseInt(e.target.value)
|
||||
setAvatar(newData)
|
||||
}}
|
||||
className="range range-success"
|
||||
step="1" />
|
||||
<div className="flex justify-between px-2.5 mt-2 text-xs">
|
||||
{Array.from({ length: skillInfo?.MaxLevel }, (_, index) => index + 1).map((index) => (
|
||||
<span key={index}>{index}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : skillInfo?.MaxLevel && skillInfo?.MaxLevel === 1 && traceButtons?.find((btn) => btn.id === skillIDSelected)?.size !== "big" ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={avatarData?.data.skills?.[skillInfo?.PointID] === 1}
|
||||
className="toggle toggle-success"
|
||||
onChange={(e) => {
|
||||
if (traceButtons?.find((btn) => btn.id === skillIDSelected)?.size === "special") {
|
||||
if (e.target.checked) {
|
||||
const newData = structuredClone(avatarData)
|
||||
newData.data.skills[skillInfo?.PointID] = 1
|
||||
setAvatar(newData)
|
||||
return
|
||||
}
|
||||
const newData = structuredClone(avatarData)
|
||||
delete newData.data.skills[skillInfo?.PointID]
|
||||
setAvatar(newData)
|
||||
return
|
||||
}
|
||||
handlerChangeStatusTrace(e.target.checked)
|
||||
}}
|
||||
/>
|
||||
<div className="font-bold text-success">
|
||||
{avatarData?.data.skills?.[skillInfo?.PointID] === 1 ? transI18n("active") : transI18n("inactive")}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
null
|
||||
)}
|
||||
|
||||
{((skillInfo?.PointName && skillInfo?.PointDesc) ||
|
||||
(skillInfo?.PointName && skillInfo?.StatusAddList.length > 0))
|
||||
&& (
|
||||
<div className="text-xl font-bold flex items-center gap-2 mt-2">
|
||||
{getLocaleName(locale, skillInfo.PointName)}
|
||||
{skillInfo.StatusAddList.length > 0 && (
|
||||
<div>
|
||||
{skillInfo.StatusAddList.map((status, index) => (
|
||||
<div key={index}>
|
||||
<div className="text-xl font-bold">{getTraceBuffDisplay(status)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{skillInfo?.PointDesc && (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
getLocaleName(locale, skillInfo?.PointDesc) || "",
|
||||
skillInfo?.Param || []
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{skillInfo?.LevelUpSkillID
|
||||
&& skillInfo?.LevelUpSkillID.length > 0
|
||||
&& dataLevelUpSkill
|
||||
&& (
|
||||
<div className="mt-2 flex flex-col gap-2">
|
||||
|
||||
{dataLevelUpSkill?.data?.map((skill, index) => (
|
||||
<div key={index}>
|
||||
|
||||
<div className="text-xl font-bold text-primary">
|
||||
{transI18n(dataLevelUpSkill.isServant ? `${skill?.AttackType ? "severaltalent" : "servantskill"}` : `${skill?.AttackType ? skill?.AttackType.toLowerCase() : "talent"}`)}
|
||||
{` (${transI18n(skill?.SkillEffect?.toLowerCase())})`}
|
||||
</div>
|
||||
|
||||
<SkillDescription
|
||||
skill={skill}
|
||||
locale={locale}
|
||||
avatarData={avatarData}
|
||||
skillInfo={skillInfo}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +1,41 @@
|
||||
import { getLocaleName, replaceByParam } from "@/helper";
|
||||
import { AvatarStore, SkillDetail, SkillTreePoint } from "@/types";
|
||||
import ExtraEffectList from "../extraInfo";
|
||||
|
||||
export const SkillDescription = ({ skill, locale, avatarData, skillInfo }: {
|
||||
skill: SkillDetail,
|
||||
locale: string,
|
||||
avatarData: AvatarStore,
|
||||
skillInfo: SkillTreePoint
|
||||
}) => {
|
||||
const levelKey = avatarData?.data.skills?.[skillInfo?.PointID]?.toString() || "";
|
||||
const params = skill.Level[levelKey]?.Param || [];
|
||||
const descHtml = getLocaleName(locale, skill.Desc) || getLocaleName(locale, skill.SimpleDesc);
|
||||
|
||||
const extraList = Object.values(skill.Extra).length > 0
|
||||
? skill.Extra
|
||||
: skill?.SimpleExtra || {};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="space-y-2 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1 h-5 bg-primary/80 rounded-sm" />
|
||||
<div
|
||||
className="text-lg font-bold tracking-wide text-foreground uppercase"
|
||||
dangerouslySetInnerHTML={{ __html: replaceByParam(getLocaleName(locale, skill.Name), []) }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="text-[15px] leading-relaxed text-foreground/90 pl-3 border-l border-transparent"
|
||||
dangerouslySetInnerHTML={{ __html: replaceByParam(descHtml, params) }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{Object.keys(extraList).length > 0 && (
|
||||
<ExtraEffectList extras={extraList} locale={locale} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
import { getLocaleName, replaceByParam } from "@/helper";
|
||||
import { AvatarStore, SkillDetail, SkillTreePoint } from "@/types";
|
||||
import ExtraEffectList from "../extraInfo";
|
||||
|
||||
export const SkillDescription = ({ skill, locale, avatarData, skillInfo }: {
|
||||
skill: SkillDetail,
|
||||
locale: string,
|
||||
avatarData: AvatarStore,
|
||||
skillInfo: SkillTreePoint
|
||||
}) => {
|
||||
const levelKey = avatarData?.data.skills?.[skillInfo?.PointID]?.toString() || "";
|
||||
const params = skill.Level[levelKey]?.Param || [];
|
||||
const descHtml = getLocaleName(locale, skill.Desc) || getLocaleName(locale, skill.SimpleDesc);
|
||||
|
||||
const extraList = Object.values(skill.Extra).length > 0
|
||||
? skill.Extra
|
||||
: skill?.SimpleExtra || {};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="space-y-2 pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-1 h-5 bg-primary/80 rounded-sm" />
|
||||
<div
|
||||
className="text-lg font-bold tracking-wide text-foreground uppercase"
|
||||
dangerouslySetInnerHTML={{ __html: replaceByParam(getLocaleName(locale, skill.Name), []) }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="text-[15px] leading-relaxed text-foreground/90 pl-3 border-l border-transparent"
|
||||
dangerouslySetInnerHTML={{ __html: replaceByParam(descHtml, params) }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{Object.keys(extraList).length > 0 && (
|
||||
<ExtraEffectList extras={extraList} locale={locale} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
import { PropsWithChildren, useContext } from "react";
|
||||
import { ThemeContext } from "./themeContext";
|
||||
|
||||
export function ClientThemeWrapper({ children }: PropsWithChildren) {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
return <div data-theme={theme} className="h-full">{children}</div>;
|
||||
"use client";
|
||||
import { PropsWithChildren, useContext } from "react";
|
||||
import { ThemeContext } from "./themeContext";
|
||||
|
||||
export function ClientThemeWrapper({ children }: PropsWithChildren) {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
return <div data-theme={theme} className="h-full">{children}</div>;
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./clientThemeWrapper"
|
||||
export * from "./themeContext"
|
||||
export * from "./clientThemeWrapper"
|
||||
export * from "./themeContext"
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
"use client";
|
||||
import { createContext, PropsWithChildren, useEffect } from "react";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
|
||||
interface ThemeContextType {
|
||||
theme?: string;
|
||||
changeTheme?: (nextTheme: string | null) => void;
|
||||
}
|
||||
export const ThemeContext = createContext<ThemeContextType>({});
|
||||
|
||||
export const ThemeProvider = ({ children }: PropsWithChildren) => {
|
||||
|
||||
const { theme, setTheme } = useLocaleStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const storedTheme = localStorage.getItem("theme");
|
||||
if (storedTheme) setTheme(storedTheme);
|
||||
}
|
||||
}, [setTheme]);
|
||||
|
||||
const changeTheme = (nextTheme: string | null) => {
|
||||
if (nextTheme) {
|
||||
setTheme(nextTheme);
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("theme", nextTheme);
|
||||
}
|
||||
} else {
|
||||
setTheme(theme === "winter" ? "night" : "winter");
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("theme", theme);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, changeTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
"use client";
|
||||
import { createContext, PropsWithChildren, useEffect } from "react";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
|
||||
interface ThemeContextType {
|
||||
theme?: string;
|
||||
changeTheme?: (nextTheme: string | null) => void;
|
||||
}
|
||||
export const ThemeContext = createContext<ThemeContextType>({});
|
||||
|
||||
export const ThemeProvider = ({ children }: PropsWithChildren) => {
|
||||
|
||||
const { theme, setTheme } = useLocaleStore()
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const storedTheme = localStorage.getItem("theme");
|
||||
if (storedTheme) setTheme(storedTheme);
|
||||
}
|
||||
}, [setTheme]);
|
||||
|
||||
const changeTheme = (nextTheme: string | null) => {
|
||||
if (nextTheme) {
|
||||
setTheme(nextTheme);
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("theme", nextTheme);
|
||||
}
|
||||
} else {
|
||||
setTheme(theme === "winter" ? "night" : "winter");
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("theme", theme);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, changeTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user