UPDATE: Replace lodash clone deep to structuredClone
All checks were successful
Gitea Auto Deploy / Deploy-Container (push) Successful in 1m47s

This commit is contained in:
2025-11-03 12:22:10 +07:00
parent 35095c19d5
commit 189002cf44
13 changed files with 78 additions and 91 deletions

View File

@@ -10,7 +10,6 @@
"fast-average-color": "^9.5.0",
"framer-motion": "^12.12.1",
"html2canvas-pro": "^1.5.11",
"lodash": "^4.17.21",
"lucide-react": "^0.536.0",
"next": "15.3.2",
"next-intl": "^4.1.0",
@@ -27,7 +26,6 @@
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/jest": "^29.5.14",
"@types/lodash": "^4.17.20",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
@@ -302,8 +300,6 @@
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
"@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="],
"@types/node": ["@types/node@20.19.9", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw=="],
"@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="],

View File

@@ -15,7 +15,6 @@
"fast-average-color": "^9.5.0",
"framer-motion": "^12.12.1",
"html2canvas-pro": "^1.5.11",
"lodash": "^4.17.21",
"lucide-react": "^0.536.0",
"next": "15.3.2",
"next-intl": "^4.1.0",
@@ -32,7 +31,6 @@
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/jest": "^29.5.14",
"@types/lodash": "^4.17.20",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",

View File

@@ -425,6 +425,7 @@ export default function AvatarInfo() {
<Image
width={904}
height={1260}
priority
src={`https://api.hakush.in/hsr/UI/lightconemaxfigures/${lightconeDetail.id}.webp`}
className="w-full h-full rounded-lg object-cover shadow-lg"
alt="Lightcone"

View File

@@ -23,54 +23,54 @@ export default function EidolonsInfo() {
return mapAvatarInfo[avatarSelected.id]?.Enhanced[avatar?.enhanced].Ranks
}
return mapAvatarInfo[avatarSelected.id]?.Ranks
}, [avatarSelected, avatars, locale, mapAvatarInfo]);
}, [avatarSelected, avatars, locale, mapAvatarInfo]);
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-gradient-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 cursor-pointer hover:scale-105"
onClick={() => {
let newRank = Number(key)
if (avatars[avatarSelected?.id || ""]?.data?.rank == Number(key)) {
newRank = Number(key) - 1
}
setAvatars({ ...avatars, [avatarSelected?.id || ""]: { ...avatars[avatarSelected?.id || ""], data: { ...avatars[avatarSelected?.id || ""].data, rank: newRank } } })
}}
>
<Image
loading="lazy"
className={`w-60 h-60 mb-2 ${Number(key) <= avatars[avatarSelected?.id || ""]?.data?.rank ? "" : "grayscale"}`}
src={`https://api.hakush.in/hsr/UI/rank/_dependencies/textures/${avatarSelected?.id}/${avatarSelected?.id}_Rank_${key}.webp`}
alt={`Rank ${key}`}
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">{key}.</span>
<ParseText
locale={locale}
text={rank.Name}
className="text-center text-base font-normal leading-tight"
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-gradient-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 cursor-pointer hover:scale-105"
onClick={() => {
let newRank = Number(key)
if (avatars[avatarSelected?.id || ""]?.data?.rank == Number(key)) {
newRank = Number(key) - 1
}
setAvatars({ ...avatars, [avatarSelected?.id || ""]: { ...avatars[avatarSelected?.id || ""], data: { ...avatars[avatarSelected?.id || ""].data, rank: newRank } } })
}}
>
<Image
className={`w-60 object-contain mb-2 ${Number(key) <= avatars[avatarSelected?.id || ""]?.data?.rank ? "" : "grayscale"}`}
src={`https://api.hakush.in/hsr/UI/rank/_dependencies/textures/${avatarSelected?.id}/${avatarSelected?.id}_Rank_${key}.webp`}
alt={`Rank ${key}`}
priority
width={240}
height={240}
/>
</div>
<div className="text-sm font-normal">
<div dangerouslySetInnerHTML={{ __html: replaceByParam(rank.Desc, rank.ParamList) }} />
</div>
</div>
))}
</div>
)}
</div>
<div className="text-lg pb-1 flex items-center justify-items-center gap-2">
<span className="inline-block text-indigo-500">{key}.</span>
<ParseText
locale={locale}
text={rank.Name}
className="text-center text-base font-normal leading-tight"
/>
</div>
<div className="text-sm font-normal">
<div dangerouslySetInnerHTML={{ __html: replaceByParam(rank.Desc, rank.ParamList) }} />
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}

View File

@@ -8,7 +8,6 @@ import useUserDataStore from "@/stores/userDataStore";
import useMonsterStore from "@/stores/monsterStore";
import Image from "next/image";
import { MonsterStore } from "@/types";
import cloneDeep from 'lodash/cloneDeep'
import useMazeStore from "@/stores/mazeStore";
import { useTranslations } from "next-intl";
import { listCurrentLanguageApi } from "@/constant/constant";
@@ -55,7 +54,7 @@ export default function AsBar() {
useEffect(() => {
if (!challengeSelected || as_config.event_id === 0 || as_config.challenge_id === 0) return
const newBattleConfig = cloneDeep(as_config)
const newBattleConfig = structuredClone(as_config)
newBattleConfig.cycle_count = 0
newBattleConfig.blessings = []

View File

@@ -17,7 +17,6 @@ import useLocaleStore from "@/stores/localeStore";
import { getLocaleName } from "@/helper";
import Image from "next/image";
import { MonsterBasic } from "@/types";
import cloneDeep from 'lodash/cloneDeep'
import { useTranslations } from "next-intl";
import { listCurrentLanguageApi } from "@/constant/constant";
import useGlobalStore from "@/stores/globalStore";
@@ -117,7 +116,7 @@ export default function CeBar() {
if (!ce_config) return
if (!extraData || !extraData.theory_craft?.mode) return
const newExtraData = cloneDeep(extraData)
const newExtraData = structuredClone(extraData)
if (!newExtraData.theory_craft.hp) {
newExtraData.theory_craft.hp = {}
}
@@ -253,7 +252,7 @@ export default function CeBar() {
onClick={(e) => {
e.stopPropagation()
const newCeConfig = cloneDeep(ce_config)
const newCeConfig = structuredClone(ce_config)
const waves = newCeConfig.monsters
const temp = waves[waveIndex - 1]
waves[waveIndex - 1] = waves[waveIndex]
@@ -270,7 +269,7 @@ export default function CeBar() {
disabled={waveIndex === ce_config.monsters.length - 1}
onClick={(e) => {
e.stopPropagation()
const newCeConfig = cloneDeep(ce_config)
const newCeConfig = structuredClone(ce_config)
const waves = newCeConfig.monsters
const temp = waves[waveIndex + 1]
waves[waveIndex + 1] = waves[waveIndex]
@@ -285,7 +284,7 @@ export default function CeBar() {
<button
onClick={(e) => {
e.stopPropagation()
const newCeConfig = cloneDeep(ce_config)
const newCeConfig = structuredClone(ce_config)
const waves = newCeConfig.monsters
const temp = waves[waveIndex]
newCeConfig.monsters.push([...temp])
@@ -298,7 +297,7 @@ export default function CeBar() {
<button
onClick={(e) => {
e.stopPropagation()
const newCeConfig = cloneDeep(ce_config)
const newCeConfig = structuredClone(ce_config)
newCeConfig.monsters.splice(waveIndex, 1)
setCeConfig(newCeConfig)
}}
@@ -316,7 +315,7 @@ export default function CeBar() {
<button
className="btn btn-xs btn-success absolute -top-2 right-12 opacity-50 group-hover:opacity-100 transition-opacity"
onClick={() => {
const newCeConfig = cloneDeep(ce_config)
const newCeConfig = structuredClone(ce_config)
newCeConfig.monsters[waveIndex].push({
monster_id: Number(member.monster_id),
@@ -331,7 +330,7 @@ export default function CeBar() {
<button
className="btn btn-xs btn-error absolute -top-2 right-2 opacity-50 group-hover:opacity-100 transition-opacity"
onClick={() => {
const newCeConfig = cloneDeep(ce_config)
const newCeConfig = structuredClone(ce_config)
newCeConfig.monsters[waveIndex].splice(memberIndex, 1)
setCeConfig(newCeConfig)
}}
@@ -379,7 +378,7 @@ export default function CeBar() {
if (isNaN(val) || val < 1 || val > 95) return
if (ce_config.monsters[waveIndex][memberIndex].level === val) return
const newCeConfig = cloneDeep(ce_config)
const newCeConfig = structuredClone(ce_config)
newCeConfig.monsters[waveIndex][memberIndex].level = val
setCeConfig(newCeConfig)
}}
@@ -397,7 +396,7 @@ export default function CeBar() {
const val = Number(e.target.value)
if (isNaN(val) || val < 0) return
const newData = cloneDeep(extraData)
const newData = structuredClone(extraData)
if (!newData?.theory_craft?.hp?.[(waveIndex + 1).toString()]) {
newData.theory_craft.hp[(waveIndex + 1).toString()] = []
@@ -473,7 +472,7 @@ export default function CeBar() {
key={monster.id}
className="flex items-center gap-2 p-2 hover:bg-success/40 rounded cursor-pointer"
onClick={() => {
const newCeConfig = cloneDeep(ce_config)
const newCeConfig = structuredClone(ce_config)
newCeConfig.monsters[waveIndex].push({
monster_id: Number(monster.id),
level: 95,
@@ -543,7 +542,7 @@ export default function CeBar() {
<div className="card-body p-8">
<button
onClick={() => {
const newCeConfig = cloneDeep(ce_config)
const newCeConfig = structuredClone(ce_config)
newCeConfig.monsters.push([])
setCeConfig(newCeConfig)
}}

View File

@@ -8,10 +8,9 @@ import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import useMonsterStore from "@/stores/monsterStore";
import Image from "next/image";
import { MonsterStore } from "@/types";
import cloneDeep from 'lodash/cloneDeep'
import useMazeStore from "@/stores/mazeStore";
import { useTranslations } from "next-intl";
import { MonsterStore } from "@/types";
export default function MocBar() {
const { MOCEvent, mapMOCInfo } = useEventStore()
@@ -32,7 +31,7 @@ export default function MocBar() {
useEffect(() => {
const challenge = mapMOCInfo[moc_config.event_id.toString()]?.find((moc) => moc.Id === moc_config.challenge_id)
if (moc_config.event_id !== 0 && moc_config.challenge_id !== 0 && challenge) {
const newBattleConfig = cloneDeep(moc_config)
const newBattleConfig = structuredClone(moc_config)
newBattleConfig.cycle_count = 0
if (moc_config.use_cycle_count) {
newBattleConfig.cycle_count = challenge.Countdown

View File

@@ -7,7 +7,6 @@ import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import useMonsterStore from "@/stores/monsterStore";
import Image from "next/image";
import cloneDeep from 'lodash/cloneDeep'
import { useTranslations } from "next-intl";
import { MonsterStore } from "@/types";
@@ -39,7 +38,7 @@ export default function PeakBar() {
}, [peak_config, mapPEAKInfo])
const challengeSelected = useMemo(() => {
const challenge = cloneDeep(listFloor.find((peak) => peak.Id === peak_config.challenge_id))
const challenge = structuredClone(listFloor.find((peak) => peak.Id === peak_config.challenge_id))
if (
challenge
&& challenge.Id === mapPEAKInfo?.[peak_config?.event_id?.toString()]?.BossLevel?.Id
@@ -57,7 +56,7 @@ export default function PeakBar() {
useEffect(() => {
if (!challengeSelected) return
if (peak_config.event_id !== 0 && peak_config.challenge_id !== 0 && challengeSelected) {
const newBattleConfig = cloneDeep(peak_config)
const newBattleConfig = structuredClone(peak_config)
newBattleConfig.cycle_count = 6
newBattleConfig.blessings = []
for (const value of challengeSelected.TagList) {

View File

@@ -8,7 +8,6 @@ import useUserDataStore from "@/stores/userDataStore";
import useMonsterStore from "@/stores/monsterStore";
import Image from "next/image";
import { MonsterStore } from "@/types";
import cloneDeep from 'lodash/cloneDeep'
import useMazeStore from "@/stores/mazeStore";
import { useTranslations } from "next-intl";
@@ -35,7 +34,7 @@ export default function PfBar() {
if (!challengeSelected || pf_config.event_id === 0 || pf_config.challenge_id === 0) {
return
}
const newBattleConfig = cloneDeep(pf_config)
const newBattleConfig = structuredClone(pf_config)
newBattleConfig.cycle_count = 4
newBattleConfig.blessings = []
if (pf_config.buff_id !== 0) {

View File

@@ -11,8 +11,7 @@ import useAvatarStore from '@/stores/avatarStore';
import useModelStore from '@/stores/modelStore';
import useRelicMakerStore from '@/stores/relicMakerStore';
import { toast } from 'react-toastify';
import { useTranslations } from 'next-intl';
import cloneDeep from 'lodash/cloneDeep'
import { useTranslations } from 'next-intl'
import { ChevronDown, ChevronUp } from 'lucide-react';
import { AnimatePresence, motion } from 'framer-motion';
@@ -81,7 +80,7 @@ export default function RelicMaker() {
const mainProp = mainAffixMap[selectedMainStat]?.property;
if (!mainProp) return;
const newSubAffixes = cloneDeep(listSelectedSubStats);
const newSubAffixes = structuredClone(listSelectedSubStats);
let updated = false;
for (let i = 0; i < newSubAffixes.length; i++) {
@@ -126,7 +125,7 @@ export default function RelicMaker() {
const handleSubStatChange = (key: string, index: number, rollCount: number, stepCount: number) => {
setError("");
const newSubAffixes = cloneDeep(listSelectedSubStats);
const newSubAffixes = structuredClone(listSelectedSubStats);
if (!subAffixOptions[key]) {
newSubAffixes[index].affixId = "";
newSubAffixes[index].property = "";
@@ -151,8 +150,8 @@ export default function RelicMaker() {
const keys = Object.keys(preSelectedSubStats[index]);
if (keys.length <= 1) return;
const newSubAffixes = cloneDeep(listSelectedSubStats);
const listHistory = cloneDeep(preSelectedSubStats[index]);
const newSubAffixes = structuredClone(listSelectedSubStats);
const listHistory = structuredClone(preSelectedSubStats[index]);
const secondLastKey = listHistory.length - 2;
const preSubAffixes = { ...listHistory[secondLastKey] };
newSubAffixes[index].rollCount = preSubAffixes.rollCount;
@@ -162,7 +161,7 @@ export default function RelicMaker() {
};
const resetSubStat = (index: number) => {
const newSubAffixes = cloneDeep(listSelectedSubStats);
const newSubAffixes = structuredClone(listSelectedSubStats);
resetHistory(index);
newSubAffixes[index].affixId = "";
newSubAffixes[index].property = "";
@@ -172,7 +171,7 @@ export default function RelicMaker() {
};
const randomizeStats = () => {
const newSubAffixes = cloneDeep(listSelectedSubStats);
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));
@@ -192,7 +191,7 @@ export default function RelicMaker() {
};
const randomizeRolls = () => {
const newSubAffixes = cloneDeep(listSelectedSubStats);
const newSubAffixes = structuredClone(listSelectedSubStats);
const randomRolls = randomPartition(9, listSelectedSubStats.length);
for (let i = 0; i < listSelectedSubStats.length; i++) {
newSubAffixes[i].rollCount = randomRolls[i];

View File

@@ -739,6 +739,7 @@ export default function ShowCaseInfo() {
alt="Lightcone Image"
width={904}
height={1206}
priority
style={{
top: '0px',
left: '6px',

View File

@@ -10,7 +10,6 @@ import Image from "next/image";
import { replaceByParam } from "@/helper";
import { mappingStats } from "@/constant/constant";
import { StatusAddType } from "@/types";
import cloneDeep from "lodash/cloneDeep";
import { toast } from "react-toastify";
export default function SkillsInfo() {
@@ -115,7 +114,7 @@ export default function SkillsInfo() {
toast.error(transI18n("maxAllFailed"))
return
}
const newData = cloneDeep(avatarData)
const newData = structuredClone(avatarData)
newData.data.skills = Object.values(avatarSkillTree).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
@@ -129,7 +128,7 @@ export default function SkillsInfo() {
const handlerChangeStatusTrace = (status: boolean) => {
if (!avatarData || !skillInfo) return
const newData = cloneDeep(avatarData)
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = status ? 1 : 0
if (!status && traceLink?.[avatarSelected?.baseType || ""]?.[skillSelected || ""]) {
@@ -286,7 +285,7 @@ export default function SkillsInfo() {
max={skillInfo?.MaxLevel || 1}
value={avatarData?.data.skills?.[skillInfo?.PointID] || 1}
onChange={(e) => {
const newData = cloneDeep(avatarData)
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = parseInt(e.target.value)
setAvatar(newData)
}}
@@ -308,12 +307,12 @@ export default function SkillsInfo() {
onChange={(e) => {
if (traceButtons?.find((btn) => btn.id === skillSelected)?.size === "special") {
if (e.target.checked) {
const newData = cloneDeep(avatarData)
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = 1
setAvatar(newData)
return
}
const newData = cloneDeep(avatarData)
const newData = structuredClone(avatarData)
delete newData.data.skills[skillInfo?.PointID]
setAvatar(newData)
return

View File

@@ -1,6 +1,4 @@
import { create } from 'zustand'
import cloneDeep from 'lodash/cloneDeep'
interface RelicMakerState {
selectedRelicSlot: string;
selectedMainStat: string;
@@ -185,14 +183,14 @@ const useRelicMakerStore = create<RelicMakerState>((set) => ({
return {}
}),
popHistory: (index: number) => set((state) => {
const newPreSelectedSubStats = cloneDeep(state.preSelectedSubStats);
const newPreSelectedSubStats = structuredClone(state.preSelectedSubStats);
const copied = state.preSelectedSubStats[index].map(item => ({ ...item }));
copied.pop();
newPreSelectedSubStats[index] = copied;
return { preSelectedSubStats: newPreSelectedSubStats };
}),
addHistory: (index, affix) => set((state) => {
const newPreSelectedSubStats = cloneDeep(state.preSelectedSubStats);
const newPreSelectedSubStats = structuredClone(state.preSelectedSubStats);
const currentList = state.preSelectedSubStats[index];
const copied = currentList ? currentList.map(item => ({ ...item })) : [
@@ -203,7 +201,7 @@ const useRelicMakerStore = create<RelicMakerState>((set) => ({
];
if (copied) {
const newAffix = cloneDeep(affix);
const newAffix = structuredClone(affix);
copied.push(newAffix);
newPreSelectedSubStats[index] = copied;
return { preSelectedSubStats: newPreSelectedSubStats };