UPDATE: monster bar
Some checks failed
Gitea Auto Deploy / Deploy-Container (push) Failing after 1m36s

This commit is contained in:
2025-07-25 09:20:39 +07:00
parent 604cf1ceec
commit 487c29def1
133 changed files with 841207 additions and 16695 deletions

View File

@@ -24,7 +24,7 @@ export default function ActionBar() {
const { avatarSelected, listRawAvatar } = useListAvatarStore()
const { setListCopyAvatar } = useCopyProfileStore()
const transI18n = useTranslations("DataPage")
const { locale, setLocale } = useLocaleStore()
const { locale } = useLocaleStore()
const { isOpenCreateProfile, setIsOpenCreateProfile, isOpenCopy, setIsOpenCopy } = useModelStore()
const { avatars, setAvatar } = useUserDataStore()
const [profileName, setProfileName] = useState("");
@@ -106,7 +106,7 @@ export default function ActionBar() {
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenCreateProfile]);
}, [isOpenCopy, isOpenCreateProfile]);
const actionMove = (path: string) => {
router.push(`/${path}`)
@@ -162,7 +162,7 @@ export default function ActionBar() {
{avatarSelected && (
<>
<Image
src={"https://api.hakush.in/hsr/UI/element/" + avatarSelected.damageType.toLowerCase() + ".webp"}
src={ `/icon/${avatarSelected.damageType.toLowerCase()}.webp`}
alt={'fire'}
className="h-[40px] w-[40px] object-contain"
width={100}
@@ -209,7 +209,7 @@ export default function ActionBar() {
<li key={index} className="grid grid-cols-12">
<button
className={`col-span-8 btn btn-ghost`}
onClick={(e) => handleProfileSelect(index)}
onClick={() => handleProfileSelect(index)}
>
<span className="flex-1 truncate text-left">
{profile.profile_name}

View File

@@ -1,91 +1,31 @@
"use client"
import { getCharacterListApi, fetchCharactersByIdsNative, getConfigMazeApi, getLightconeListApi, fetchLightconesByIdsNative, fetchRelicsByIdsNative, getRelicSetListApi, getMainAffixApi, getSubAffixApi } from "@/lib/api"
import Image from "next/image"
import { useEffect, useState } from "react"
import CharacterCard from "../card/characterCard"
import useLocaleStore from "@/stores/localeStore"
import { listCurrentLanguageApi } from "@/lib/constant"
import useAvatarStore from "@/stores/avatarStore"
import useUserDataStore from "@/stores/userDataStore"
import { converterToAvatarStore, getAvatarNotExist } from "@/helper"
import useLightconeStore from "@/stores/lightconeStore"
import useRelicStore from "@/stores/relicStore"
import useAffixStore from "@/stores/affixStore"
import useMazeStore from "@/stores/mazeStore"
import { useTranslations } from "next-intl"
import { useFetchASData, useFetchAvatarData, useFetchConfigData, useFetchLightconeData, useFetchMOCData, useFetchMonsterData, useFetchPFData, useFetchRelicData } from "@/hooks"
export default function AvatarBar() {
const [listElement, setListElement] = useState<Record<string, boolean>>({ "fire": false, "ice": false, "imaginary": false, "physical": false, "quantum": false, "thunder": false, "wind": false })
const [listPath, setListPath] = useState<Record<string, boolean>>({ "knight": false, "mage": false, "priest": false, "rogue": false, "shaman": false, "warlock": false, "warrior": false, "memory": false })
const { listAvatar, setListAvatar, setAvatarSelected, setFilter, filter, setAllMapAvatarInfo, avatarSelected } = useAvatarStore()
const { setAvatars, avatars } = useUserDataStore()
const { listAvatar, setAvatarSelected, setFilter, filter } = useAvatarStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore()
const { setListLightcone, setAllMapLightconeInfo } = useLightconeStore()
const { setListRelic, setAllMapRelicInfo } = useRelicStore()
const { setMapMainAffix, setMapSubAffix } = useAffixStore()
const { setAllData } = useMazeStore()
useEffect(() => {
const fetchData = async () => {
// Maze
const maze = await getConfigMazeApi()
setAllData(maze)
// Affix
const mapMainAffix = await getMainAffixApi()
setMapMainAffix(mapMainAffix)
const mapSubAffix = await getSubAffixApi()
setMapSubAffix(mapSubAffix)
}
fetchData()
}, [])
useEffect(() => {
const fetchData = async () => {
// Avatar
const listAvatar = await getCharacterListApi()
listAvatar.sort((a, b) => {
const aHasRelease = typeof a.release === 'number';
const bHasRelease = typeof b.release === 'number';
if (!aHasRelease && !bHasRelease) return 0;
if (!aHasRelease) return -1;
if (!bHasRelease) return 1;
return b.release! - a.release!;
});
const mapAvatar = await fetchCharactersByIdsNative(listAvatar.map((item) => item.id), listCurrentLanguageApi[locale.toLowerCase()])
setListAvatar(listAvatar)
setAllMapAvatarInfo(mapAvatar)
const avatarStore = converterToAvatarStore(getAvatarNotExist())
if (Object.keys(avatarStore).length > 0) {
setAvatars({ ...avatars, ...avatarStore })
}
if (!avatarSelected) {
setAvatarSelected(listAvatar[0])
}
// Lightcone
const listLightcone = await getLightconeListApi()
listLightcone.sort((a, b) => Number(b.id) - Number(a.id))
setListLightcone(listLightcone)
const mapLightcone = await fetchLightconesByIdsNative(listLightcone.map((item) => item.id), listCurrentLanguageApi[locale.toLowerCase()])
setAllMapLightconeInfo(mapLightcone)
// Relic
const listRelic = await getRelicSetListApi()
setListRelic(listRelic)
const mapRelic = await fetchRelicsByIdsNative(listRelic.map((item) => item.id), listCurrentLanguageApi[locale.toLowerCase()])
setAllMapRelicInfo(mapRelic)
}
fetchData()
}, [locale])
useFetchConfigData()
useFetchAvatarData()
useFetchLightconeData()
useFetchRelicData()
useFetchMonsterData()
useFetchPFData()
useFetchMOCData()
useFetchASData()
useEffect(() => {
setFilter({ ...filter, locale: locale, element: Object.keys(listElement).filter((key) => listElement[key]), path: Object.keys(listPath).filter((key) => listPath[key]) })
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [locale, listElement, listPath])
return (
@@ -103,7 +43,7 @@ export default function AvatarBar() {
/>
</div>
<div className="grid grid-cols-1 md: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(listElement).map(([key, value], index) => (
{Object.keys(listElement).map((key, index) => (
<div
key={index}
onClick={() => {
@@ -113,7 +53,7 @@ export default function AvatarBar() {
style={{
backgroundColor: listElement[key] ? "#374151" : "#6B7280"
}}>
<Image src={"https://api.hakush.in/hsr/UI/element/" + key + ".webp"}
<Image src={ `/icon/${key}.webp`}
alt={key}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md"
width={200}
@@ -123,7 +63,7 @@ export default function AvatarBar() {
</div>
<div className="grid grid-cols-1 md:grid-cols-4 lg:grid-cols-8 mb-1 mx-1 gap-2 overflow-y-auto w-full max-h-[17vh] min-h-[5vh]">
{Object.entries(listPath).map(([key, value], index) => (
{Object.keys(listPath).map((key, index) => (
<div
key={index}
onClick={() => {
@@ -135,7 +75,7 @@ export default function AvatarBar() {
}}
>
<Image src={"https://api.hakush.in/hsr/UI/pathicon/" + key + ".webp"}
<Image src={`/icon/${key}.webp`}
alt={key}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md"
width={200}

View File

@@ -1,9 +1,8 @@
"use client"
import { useRouter } from 'next/navigation'
import useAvatarStore from "@/stores/avatarStore"
import useUserDataStore from "@/stores/userDataStore";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo } from "react";
import { motion } from "framer-motion";
import LightconeBar from '../lightconeBar'
import useLightconeStore from '@/stores/lightconeStore'
@@ -16,7 +15,6 @@ import useModelStore from '@/stores/modelStore';
import useMazeStore from '@/stores/mazeStore';
export default function AvatarInfo() {
const router = useRouter()
const { avatarSelected, mapAvatarInfo } = useAvatarStore()
const { Technique } = useMazeStore()
const { avatars, setAvatars, setAvatar } = useUserDataStore()
@@ -252,7 +250,7 @@ export default function AvatarInfo() {
className="select select-bordered select-secondary"
>
<option value="">{transI18n("origin")}</option>
{Object.entries(mapAvatarInfo[avatarSelected?.id || ""]?.Enhanced || {}).map(([key, value]) => (
{Object.keys(mapAvatarInfo[avatarSelected?.id || ""]?.Enhanced || {}).map((key) => (
<option key={key} value={key}>
{key}
</option>

View File

@@ -32,6 +32,7 @@ export default function CharacterCard({ data }: CharacterCardProps) {
width={376}
height={512}
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${data.id}.webp`}
priority={true}
className="w-full h-full rounded-md object-cover"
alt="ALT"
/>
@@ -39,14 +40,14 @@ export default function CharacterCard({ data }: CharacterCardProps) {
width={32}
height={32}
src={`https://api.hakush.in/hsr/UI/element/${data.damageType.toLowerCase()}.webp`}
src={`/icon/${data.damageType.toLowerCase()}.webp`}
className="absolute top-0 left-0 w-6 h-6 rounded-full"
alt={data.damageType.toLowerCase()}
/>
<Image
width={32}
height={32}
src={`https://api.hakush.in/hsr/UI/pathicon/${data.baseType.toLowerCase()}.webp`}
src={`/icon/${data.baseType.toLowerCase()}.webp`}
className="absolute top-0 right-0 w-6 h-6 rounded-full"
alt={data.baseType.toLowerCase()}
style={{

View File

@@ -1,11 +1,10 @@
"use client";
import React, { useState, useMemo } from 'react';
import React from 'react';
import { CharacterInfoCardType } from '@/types';
import { getNameChar, replaceByParam } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import useAvatarStore from '@/stores/avatarStore';
import useLightconeStore from '@/stores/lightconeStore';
import useRelicStore from '@/stores/relicStore';
import Image from 'next/image';
import ParseText from '../parseText';
@@ -14,7 +13,6 @@ export default function CharacterInfoCard({ character, selectedCharacters, onCha
const isSelected = selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id);
const { mapAvatarInfo } = useAvatarStore();
const { mapLightconeInfo } = useLightconeStore();
const { mapRelicInfo } = useRelicStore();
const { locale } = useLocaleStore();
return (
@@ -40,14 +38,14 @@ export default function CharacterInfoCard({ character, selectedCharacters, onCha
<Image
width={48}
height={48}
src={`https://api.hakush.in/hsr/UI/element/${mapAvatarInfo[character.avatar_id.toString()]?.DamageType.toLowerCase()}.webp`}
src={`/icon/${mapAvatarInfo[character.avatar_id.toString()]?.DamageType.toLowerCase()}.webp`}
className="absolute top-0 left-0 w-10 h-10 rounded-full"
alt={mapAvatarInfo[character.avatar_id.toString()]?.DamageType.toLowerCase()}
/>
<Image
width={48}
height={48}
src={`https://api.hakush.in/hsr/UI/pathicon/${mapAvatarInfo[character.avatar_id.toString()]?.BaseType.toLowerCase()}.webp`}
src={`/icon/${mapAvatarInfo[character.avatar_id.toString()]?.BaseType.toLowerCase()}.webp`}
className="absolute top-0 right-0 w-10 h-10 rounded-full"
alt={mapAvatarInfo[character.avatar_id.toString()]?.BaseType.toLowerCase()}
style={{

View File

@@ -1,6 +1,6 @@
"use client";
import { getNameLightcone } from '@/helper';
import { getLocaleName } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import { LightConeBasic } from '@/types';
import ParseText from '../parseText';
@@ -12,7 +12,7 @@ interface LightconeCardProps {
export default function LightconeCard({ data }: LightconeCardProps) {
const { locale } = useLocaleStore();
const text = getNameLightcone(locale, data)
const text = getLocaleName(locale, data)
return (
<li className="z-10 flex flex-col items-center rounded-md shadow-lg
bg-gradient-to-b from-customStart to-customEnd transform transition-transform duration-300

View File

@@ -1,10 +1,9 @@
"use client";
import React from 'react';
import { AvatarProfileCardType } from '@/types';
import useLocaleStore from '@/stores/localeStore';
import useAvatarStore from '@/stores/avatarStore';
import useLightconeStore from '@/stores/lightconeStore';
import useRelicStore from '@/stores/relicStore';
import Image from 'next/image';
import ParseText from '../parseText';
@@ -50,7 +49,7 @@ export default function ProfileCard({ profile, selectedProfile, onProfileToggle
{Object.keys(profile.relics).length > 0 && (
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
{Object.entries(profile.relics).map(([key, relic], index) => (
{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

View File

@@ -1,4 +1,5 @@
import useLocaleStore from "@/stores/localeStore";
"use client";
import useUserDataStore from "@/stores/userDataStore";
import Image from "next/image";
import { useMemo } from "react";
@@ -26,31 +27,31 @@ const getRarityName = (slot: string) => {
);
case '2': return (
<div className="flex items-center gap-1">
<Image src="/relics/HAND.png" alt="Head" width={20} height={20} />
<Image src="/relics/HAND.png" alt="Hand" width={20} height={20} />
<h2>Hands</h2>
</div>
);
case '3': return (
<div className="flex items-center gap-1">
<Image src="/relics/BODY.png" alt="Head" width={20} height={20} />
<Image src="/relics/BODY.png" alt="Body" width={20} height={20} />
<h2>Body</h2>
</div>
);
case '4': return (
<div className="flex items-center gap-1">
<Image src="/relics/FOOT.png" alt="Head" width={20} height={20} />
<Image src="/relics/FOOT.png" alt="Foot" width={20} height={20} />
<h2>Feet</h2>
</div>
);
case '5': return (
<div className="flex items-center gap-1">
<Image src="/relics/OBJECT.png" alt="Head" width={20} height={20} />
<Image src="/relics/NECK.png" alt="Neck" width={20} height={20} />
<h2>Planar sphere</h2>
</div>
);
case '6': return (
<div className="flex items-center gap-1">
<Image src="/relics/NECK.png" alt="Head" width={20} height={20} />
<Image src="/relics/OBJECT.png" alt="Object" width={20} height={20} />
<h2>Link rope</h2>
</div>
);
@@ -58,7 +59,6 @@ const getRarityName = (slot: string) => {
}
};
export default function RelicCard({ slot, avatarId }: RelicCardProps) {
const { locale } = useLocaleStore();
const { avatars } = useUserDataStore()
const relicDetail = useMemo(() => {

View File

@@ -1,4 +1,5 @@
"use client";
import React, { useState, useRef} from 'react';
export default function ShowCaseInfo() {

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client"
import { replaceByParam } from "@/helper";
import useListAvatarStore from "@/stores/avatarStore";
@@ -30,8 +31,8 @@ export default function EidolonsInfo() {
className="flex flex-col items-center cursor-pointer hover:scale-105"
onClick={() => {
let newRank = Number(key)
if (Number(key) == 1 && avatars[avatarSelected?.id || ""]?.data?.rank == 1) {
newRank = 0
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 } } })
}}

View File

@@ -2,7 +2,6 @@
import { connectToPS, downloadJson, syncDataToPS } from "@/helper";
import { converterToFreeSRJson } from "@/helper/converterToFreeSRJson";
import { useChangeTheme } from "@/hooks/useChangeTheme";
import { listCurrentLanguage } from "@/lib/constant";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
@@ -18,6 +17,7 @@ import { toast } from "react-toastify";
import { micsSchema } from "@/zod";
import useConnectStore from "@/stores/connectStore";
import useGlobalStore from "@/stores/globalStore";
import MonsterBar from "../monsterBar";
const themes = [
{ label: "Winter" },
@@ -29,15 +29,34 @@ const themes = [
export default function Header() {
const { changeTheme } = useChangeTheme()
const { locale, setLocale } = useLocaleStore()
const { avatars, battle_config, setAvatars, setBattleConfig } = useUserDataStore()
const {
avatars,
battle_type,
setAvatars,
setBattleType,
moc_config,
pf_config,
as_config,
ce_config,
setMocConfig,
setPfConfig,
setAsConfig,
setCeConfig,
} = useUserDataStore()
const router = useRouter()
const transI18n = useTranslations("DataPage")
const { setIsOpenImport, isOpenImport } = useModelStore()
const {
setIsOpenImport,
isOpenImport,
setIsOpenMonster,
isOpenMonster,
setIsOpenConnect,
isOpenConnect
} = useModelStore()
const [message, setMessage] = useState({ text: '', type: '' });
const [importModal, setImportModal] = useState("enka");
const [isModalOpen, setIsModalOpen] = useState(false);
const {
connectionType,
privateType,
@@ -84,17 +103,14 @@ export default function Header() {
}
const handleShow = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
setIsModalOpen(true);
modal.showModal();
}
};
// Close modal handler
const handleCloseModal = (modalId: string) => {
setIsModalOpen(false);
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.close()
@@ -107,16 +123,25 @@ export default function Header() {
handleCloseModal("import_modal");
return;
}
if (!isOpenMonster) {
handleCloseModal("monster_modal");
return;
}
if (!isOpenConnect) {
handleCloseModal("connect_modal");
return;
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
handleCloseModal("connect_modal");
handleCloseModal("import_modal");
handleCloseModal("monster_modal");
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isModalOpen, isOpenImport]);
}, [isOpenImport, isOpenMonster, isOpenConnect]);
const handleImportDatabase = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -139,9 +164,13 @@ export default function Header() {
const data = JSON.parse(e.target?.result as string);
const parsed = micsSchema.parse(data)
setAvatars(parsed.avatars)
setBattleConfig(parsed.battle_config)
setBattleType(parsed.battle_type)
setMocConfig(parsed.moc_config)
setPfConfig(parsed.pf_config)
setAsConfig(parsed.as_config)
setCeConfig(parsed.ce_config)
toast.success(transI18n("importDatabaseSuccess"))
} catch (error) {
} catch {
toast.error(transI18n("fileMustBeAValidJsonFile"))
}
};
@@ -201,25 +230,31 @@ export default function Header() {
<details>
<summary className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium">{transI18n("exportData")}</summary>
<ul className="p-2">
<li><a onClick={() => downloadJson("freesr-data", converterToFreeSRJson(avatars, battle_config))}>{transI18n("freeSr")}</a></li>
<li><a onClick={() => downloadJson("database-data", { avatars: avatars, battle_config: battle_config })}>{transI18n("database")}</a></li>
<li><a onClick={() => downloadJson("freesr-data", converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config))}>{transI18n("freeSr")}</a></li>
<li><a onClick={() => downloadJson("database-data", { avatars: avatars, battle_type: battle_type, moc_config: moc_config, pf_config: pf_config, as_config: as_config, ce_config: ce_config })}>{transI18n("database")}</a></li>
</ul>
</details>
</li>
<li>
<button
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => handleShow("connection_modal")}
onClick={() => {
setIsOpenConnect(true)
handleShow("connect_modal")
}}
>
{transI18n("connectSetting")}
</button>
</li>
<li>
<button
disabled
onClick={() => {
setIsOpenMonster(true)
handleShow("monster_modal")
}}
className="disabled px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
>
{transI18n("monsterSetting")} ({transI18n("comingSoon")})
{transI18n("monsterSetting")}
</button>
</li>
</ul>
@@ -279,25 +314,31 @@ export default function Header() {
<details>
<summary className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium">{transI18n("exportData")}</summary>
<ul className="p-2">
<li><a onClick={() => downloadJson("freesr-data", converterToFreeSRJson(avatars, battle_config))}>{transI18n("freeSr")}</a></li>
<li><a onClick={() => downloadJson("database-data", { avatars: avatars, battle_config: battle_config })}>{transI18n("database")}</a></li>
<li><a onClick={() => downloadJson("freesr-data", converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config))}>{transI18n("freeSr")}</a></li>
<li><a onClick={() => downloadJson("database-data", { avatars: avatars, battle_type: battle_type, moc_config: moc_config, pf_config: pf_config, as_config: as_config, ce_config: ce_config })}>{transI18n("database")}</a></li>
</ul>
</details>
</li>
<li>
<button
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => handleShow("connection_modal")}
onClick={() => {
setIsOpenConnect(true)
handleShow("connect_modal")
}}
>
{transI18n("connectSetting")}
</button>
</li>
<li>
<button
disabled
className="disabled:opacity-50 px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => {
setIsOpenMonster(true)
handleShow("monster_modal")
}}
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
>
{transI18n("monsterSetting")} ({transI18n("comingSoon")})
{transI18n("monsterSetting")}
</button>
</li>
</ul>
@@ -409,14 +450,17 @@ export default function Header() {
</Link>
</div>
<dialog id="connection_modal" className="modal sm:modal-middle backdrop-blur-sm">
<dialog id="connect_modal" className="modal sm:modal-middle backdrop-blur-sm">
<div className="modal-box w-11/12 max-w-7xl 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={() => handleCloseModal("connection_modal")}
onClick={() => {
setIsOpenConnect(false)
handleCloseModal("connect_modal")
}}
>
</motion.button>
@@ -597,6 +641,32 @@ export default function Header() {
{importModal === "freesr" && <FreeSRImport />}
</div>
</dialog>
<dialog id="monster_modal" className="modal sm:backdrop-blur-sm md:backdrop-blur-none">
<div className="modal-box w-11/12 max-w-max 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={() => {
handleCloseModal("monster_modal")
setIsOpenMonster(false)
}}
>
</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-gradient-to-r from-pink-400 to-cyan-400">
{transI18n("monsterSetting")}
</h3>
</div>
<MonsterBar />
</div>
</dialog>
</div>
)
}

View File

@@ -1,16 +1,15 @@
"use client"
import useModelStore from "@/stores/modelStore";
import useUserDataStore from "@/stores/userDataStore";
import { useEffect, useState } from "react";
import useCopyProfileStore from "@/stores/copyProfile";
import ProfileCard from "../card/profileCard";
import { AvatarProfileCardType, AvatarProfileStore } from "@/types";
import Image from "next/image";
import SelectCustom from "../select";
import useListAvatarStore from "@/stores/avatarStore";
import { getNameChar } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import { useTranslations } from "next-intl";
import SelectCustomImage from "../select/customSelectImage";
export default function CopyImport() {
const { avatars, setAvatar } = useUserDataStore();
@@ -50,6 +49,7 @@ export default function CopyImport() {
element: Object.keys(listElement).filter((key) => listElement[key]),
rarity: Object.keys(listRank).filter((key) => listRank[key])
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listPath, listRank, locale])
const clearSelection = () => {
@@ -101,7 +101,7 @@ export default function CopyImport() {
return;
}
const newListProfile = avatars[avatarCopySelected.id.toString()].profileList.map((profile, index) => {
const newListProfile = avatars[avatarCopySelected.id.toString()].profileList.map((profile) => {
if (!profile.lightcone?.item_id && Object.keys(profile.relics).length == 0) {
return null;
}
@@ -149,7 +149,7 @@ export default function CopyImport() {
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
}}>
<Image
src={`https://api.hakush.in/hsr/UI/pathicon/${key}.webp`}
src={`/icon/${key}.webp`}
alt={key}
className="h-[32px] w-[32px] object-contain rounded-md"
width={200}
@@ -174,7 +174,7 @@ export default function CopyImport() {
backgroundColor: listElement[key] ? "#374151" : "#6B7280"
}}>
<Image
src={`https://api.hakush.in/hsr/UI/element/${key}.webp`}
src={`/icon/${key}.webp`}
alt={key}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md"
width={200}
@@ -210,7 +210,7 @@ export default function CopyImport() {
<div>
<div>{transI18n("characterName")}</div>
{listCopyAvatar.length > 0 && (
<SelectCustom
<SelectCustomImage
customSet={listCopyAvatar.map((avatar) => ({
value: avatar.id.toString(),
label: getNameChar(locale, avatar),

View File

@@ -2,7 +2,7 @@
import { useState } from "react";
import CharacterInfoCard from "../card/characterInfoCard";
import useEnkaStore from "@/stores/enkaStore";
import { SendDataThroughProxy } from "@/lib/api";
import { SendDataThroughProxy } from "@/lib/api/api";
import { CharacterInfoCardType, EnkaResponse } from "@/types";
import useUserDataStore from "@/stores/userDataStore";
import { converterOneEnkaDataToAvatarStore } from "@/helper";
@@ -21,7 +21,7 @@ export default function EnkaImport() {
} = useEnkaStore();
const transI18n = useTranslations("DataPage")
const { avatars, setAvatar } = useUserDataStore();
const { isOpenImport, setIsOpenImport } = useModelStore()
const { setIsOpenImport } = useModelStore()
const [isLoading, setIsLoading] = useState(false)
const [Error, setError] = useState("")
@@ -191,7 +191,7 @@ export default function EnkaImport() {
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{enkaData?.detailInfo.avatarDetailList.map((character, index) => (
{enkaData?.detailInfo.avatarDetailList.map((character) => (
<CharacterInfoCard
key={character.avatarId}
character={{

View File

@@ -12,8 +12,8 @@ import { converterOneFreeSRDataToAvatarStore } from "@/helper";
import { useTranslations } from "next-intl";
export default function FreeSRImport() {
const { avatars, setAvatar, setBattleConfig } = useUserDataStore();
const { isOpenImport, setIsOpenImport } = useModelStore()
const { avatars, setAvatar } = useUserDataStore();
const { setIsOpenImport } = useModelStore()
const [isLoading, setIsLoading] = useState(false)
const [Error, setError] = useState("")
const { freeSRData, setFreeSRData, selectedCharacters, setSelectedCharacters } = useFreeSRStore()
@@ -33,7 +33,7 @@ export default function FreeSRImport() {
const selectAll = () => {
if (freeSRData) {
setSelectedCharacters(Object.entries(freeSRData?.avatars).map(([key, character]) => {
setSelectedCharacters(Object.values(freeSRData?.avatars).map((character) => {
const lightcone = freeSRData.lightcones.find((lightcone) => lightcone.equip_avatar === character.avatar_id)
const relics = freeSRData.relics.filter((relic) => relic.equip_avatar === character.avatar_id)
return {
@@ -84,7 +84,7 @@ export default function FreeSRImport() {
setFreeSRData(parsed)
setError("")
setSelectedCharacters(Object.entries(parsed?.avatars || {}).map(([key, character]) => {
setSelectedCharacters(Object.values(parsed?.avatars || {}).map((character) => {
const lightcone = parsed?.lightcones.find((lightcone) => lightcone.equip_avatar === character.avatar_id)
const relics = parsed?.relics.filter((relic) => relic.equip_avatar === character.avatar_id)
return {
@@ -104,7 +104,7 @@ export default function FreeSRImport() {
})) ?? [],
} as CharacterInfoCardType
}));
} catch (error) {
} catch {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("fileIsNotAValidFreeSRJsonFile"))
@@ -128,8 +128,8 @@ export default function FreeSRImport() {
setError("");
const listAvatars = { ...avatars }
const filterData = Object.entries(freeSRData?.avatars || {}).filter(([key, character]) => selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id))
filterData.forEach(([key, character]) => {
const filterData = Object.values(freeSRData?.avatars || {}).filter((character) => selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id))
filterData.forEach((character) => {
const newAvatar = { ...listAvatars[character.avatar_id] }
if (Object.keys(newAvatar).length !== 0) {
newAvatar.level = character.level
@@ -147,7 +147,6 @@ export default function FreeSRImport() {
}
})
setBattleConfig(freeSRData?.battle_config)
setIsOpenImport(false)
toast.success(transI18n("importFreeSRDataSuccess"))
}
@@ -200,7 +199,7 @@ export default function FreeSRImport() {
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{Object.entries(freeSRData?.avatars || {}).map(([key, character]) => {
{Object.values(freeSRData?.avatars || {}).map((character) => {
const lightcone = freeSRData?.lightcones.find((lightcone) => lightcone.equip_avatar === character.avatar_id)
const relics = freeSRData?.relics.filter((relic) => relic.equip_avatar === character.avatar_id)
return (

View File

@@ -26,6 +26,7 @@ export default function LightconeBar() {
path: Object.keys(listPath).filter((key) => listPath[key]),
rarity: Object.keys(listRank).filter((key) => listRank[key])
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [listPath, listRank, locale])
return (
@@ -49,7 +50,7 @@ export default function LightconeBar() {
<div className="grid grid-cols-12 mt-1 w-full">
<div className="grid justify-items-start col-span-8">
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-8 mb-1 mx-1 gap-2">
{Object.entries(listPath).map(([key, value], index) => (
{Object.keys(listPath).map((key, index) => (
<div
key={index}
onClick={() => {
@@ -59,8 +60,8 @@ export default function LightconeBar() {
style={{
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
}}>
<Image src={"https://api.hakush.in/hsr/UI/pathicon/" + key + ".webp"}
alt={key}
<Image src={`/icon/${key}.webp`}
alt={key}
className="h-[32px] w-[32px] object-contain rounded-md "
width={200}
height={200} />
@@ -71,7 +72,7 @@ export default function LightconeBar() {
<div className="grid justify-items-end col-span-4 w-full">
<div className="grid grid-cols-1 sm:grid-cols-3 mb-1 mx-1 gap-2">
{Object.entries(listRank).map(([key, value], index) => (
{Object.keys(listRank).map((key, index) => (
<div
key={index}
onClick={() => {

View File

@@ -0,0 +1,338 @@
"use client"
import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText";
import useEventStore from "@/stores/eventStore";
import { getLocaleName, replaceByParam } from "@/helper";
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";
export default function AsBar() {
const { ASEvent, mapASInfo } = useEventStore()
const { listMonster } = useMonsterStore()
const { locale } = useLocaleStore()
const {
as_config,
setAsConfig
} = useUserDataStore()
const { AS } = useMazeStore()
const transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => {
return mapASInfo[as_config.event_id.toString()]?.Level.find((as) => as.Id === as_config.challenge_id)
}, [as_config, mapASInfo])
const eventSelected = useMemo(() => {
return mapASInfo[as_config.event_id.toString()]
}, [as_config, mapASInfo])
const buffList = useMemo(() => {
const challenge = AS[as_config.event_id.toString()];
if (!challenge) return { buffList: [], buffId: [] };
if (as_config.floor_side === "Upper" || as_config.floor_side === "Upper -> Lower") {
return {
buffList: eventSelected?.BuffList1 ?? [],
buffId: challenge.buff_1 ?? [],
};
}
if (as_config.floor_side === "Lower" || as_config.floor_side === "Lower -> Upper") {
return {
buffList: eventSelected?.BuffList2 ?? [],
buffId: challenge.buff_2 ?? [],
};
}
return { buffList: [], buffId: [] };
}, [AS, as_config.event_id, as_config.floor_side, eventSelected?.BuffList1, eventSelected?.BuffList2]);
useEffect(() => {
const challenge = mapASInfo[as_config.event_id.toString()]?.Level.find((as) => as.Id === as_config.challenge_id)
if (as_config.event_id !== 0 && as_config.challenge_id !== 0 && challenge) {
const newBattleConfig = cloneDeep(as_config)
newBattleConfig.cycle_count = 4
newBattleConfig.blessings = []
if (as_config.buff_id !== 0) {
newBattleConfig.blessings.push({
id: as_config.buff_id,
level: 1
})
}
if (AS[as_config.challenge_id.toString()]) {
newBattleConfig.blessings.push({
id: Number(AS[as_config.challenge_id.toString()].maze_buff),
level: 1
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = 0
if ((as_config.floor_side === "Upper" || as_config.floor_side === "Upper -> Lower") && challenge.EventIDList1.length > 0) {
newBattleConfig.stage_id = challenge.EventIDList1[0].StageID
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if ((as_config.floor_side === "Lower" || as_config.floor_side === "Lower -> Upper") && challenge.EventIDList2.length > 0) {
newBattleConfig.stage_id = challenge.EventIDList2[0].StageID
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if (as_config.floor_side === "Lower -> Upper" && challenge.EventIDList1.length > 0) {
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
} else if (as_config.floor_side === "Upper -> Lower" && challenge.EventIDList2.length > 0) {
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
setAsConfig(newBattleConfig)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
as_config.event_id,
as_config.challenge_id,
as_config.floor_side,
as_config.buff_id,
mapASInfo,
AS,
])
return (
<div className="container mx-auto px-4 py-8 relative">
{/* Title Card */}
<div className="rounded-xl p-4 mb-2 border border-warning">
<div className="mb-4 w-full">
<SelectCustomText
customSet={ASEvent.filter(as => as.lang.get(locale)).map((as) => ({
id: as.id,
name: getLocaleName(locale, as),
time: `${as.begin} - ${as.end}`,
}))}
excludeSet={[]}
selectedCustomSet={as_config.event_id.toString()}
placeholder={transI18n("selectASEvent")}
setSelectedCustomSet={(id) => setAsConfig({ ...as_config, event_id: Number(id), challenge_id: 0, buff_id: 0 })}
/>
</div>
{/* Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<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>
{mapASInfo[as_config.event_id.toString()]?.Level.map((as) => (
<option key={as.Id} value={as.Id}>{as.Id % 10}</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>
<option value="Upper">{transI18n("upper")}</option>
<option value="Lower">{transI18n("lower")}</option>
<option value="Upper -> Lower">{transI18n("upperToLower")}</option>
<option value="Lower -> Upper">{transI18n("lowerToUpper")}</option>
</select>
</div>
</div>
{eventSelected && (
<div className="mb-4 w-full">
<SelectCustomText
customSet={
Array.isArray(buffList?.buffList) && Array.isArray(buffList?.buffId)
? buffList.buffList.map((buff, index) => ({
id: buffList.buffId?.[index]?.toString() || "",
name: buff?.Name || "",
description: replaceByParam(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>
{eventSelected && eventSelected.Buff?.Name ? (
<div
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
eventSelected.Buff?.Desc || "",
eventSelected.Buff?.Param || []
)
}}
/>
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* First Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("firstHalfEnemies")}</h2>
{challengeSelected && challengeSelected?.EventIDList1?.length > 0 && challengeSelected?.EventIDList1[0].MonsterList.map((wave, waveIndex) => (
<div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold mb-t">{transI18n("wave")} {waveIndex + 1}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Object.values(wave).map((waveValue, enemyIndex) => (
<div
key={enemyIndex}
className="rounded-xl p-2 border border-white/10 shadow-md hover:border-white/20 hover:shadow-lg transition"
>
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(waveValue))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
</div>
<div className="flex flex-col">
<div className="text-sm font-semibold">Lv. {challengeSelected?.EventIDList1[0].Level}</div>
<div className="flex items-center space-x-1 mt-1">
{listMonster
.find((monster) => monster.child.includes(waveValue))
?.weak?.map((icon, iconIndex) => (
<Image
src={`/icon/${icon.toLowerCase()}.webp`}
alt={icon}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md border border-white/20 shadow-sm"
width={200}
height={200}
key={iconIndex}
/>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
{/* Second Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("secondHalfEnemies")}</h2>
{challengeSelected && challengeSelected?.EventIDList2?.length > 0 && challengeSelected?.EventIDList2[0].MonsterList.map((wave, waveIndex) => (
<div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold mb-t">{transI18n("wave")} {waveIndex + 1}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Object.values(wave).map((waveValue, enemyIndex) => (
<div
key={enemyIndex}
className="rounded-xl p-2 border border-white/10 shadow-md hover:border-white/20 hover:shadow-lg transition"
>
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(waveValue))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
</div>
<div className="flex flex-col">
<div className="text-sm font-semibold">Lv. {challengeSelected?.EventIDList1[0].Level}</div>
<div className="flex items-center space-x-1 mt-1">
{listMonster
.find((monster) => monster.child.includes(waveValue))
?.weak?.map((icon, iconIndex) => (
<Image
src={`/icon/${icon.toLowerCase()}.webp`}
alt={icon}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md border border-white/20 shadow-sm"
width={200}
height={200}
key={iconIndex}
/>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,364 @@
"use client";
import React, { useEffect, useMemo, useState } from "react";
import {
Plus,
Trash2,
ChevronUp,
ChevronDown,
Search,
} from "lucide-react";
import useMazeStore from "@/stores/mazeStore";
import useUserDataStore from "@/stores/userDataStore";
import useMonsterStore from "@/stores/monsterStore";
import useLocaleStore from "@/stores/localeStore";
import { getLocaleName } from "@/helper";
import Image from "next/image";
import { MonsterBasic } from "@/types";
import { cloneDeep } from "lodash";
import { useTranslations } from "next-intl";
export default function CeBar() {
const [searchTerm, setSearchTerm] = useState("");
const [showSearchWaveId, setShowSearchWaveId] = useState<number | null>(null);
const { Stage } = useMazeStore()
const { ce_config, setCeConfig } = useUserDataStore()
const { listMonster } = useMonsterStore()
const { locale } = useLocaleStore()
const filteredMonsters = useMemo(() => {
const newlistMonster = new Set<MonsterBasic>()
for (const monster of listMonster) {
if (getLocaleName(locale, monster).toLowerCase().includes(searchTerm.toLowerCase())) {
newlistMonster.add(monster)
}
if (monster.id.toLowerCase().includes(searchTerm.toLowerCase())) {
newlistMonster.add(monster)
}
}
return Array.from(newlistMonster)
}, [listMonster, locale, searchTerm]);
const transI18n = useTranslations("DataPage")
const [showSearchStage, setShowSearchStage] = useState(false)
const [stageSearchTerm, setStageSearchTerm] = useState("")
const [stagePage, setStagePage] = useState(1)
const pageSize = 30
const stageList = useMemo(() => Object.values(Stage).map((stage) => ({
id: stage.stage_id.toString(),
name: `${stage.stage_type} (${stage.stage_id})`,
})), [Stage])
const filteredStages = useMemo(() => stageList.filter((s) =>
s.name.toLowerCase().includes(stageSearchTerm.toLowerCase())
), [stageList, stageSearchTerm])
const paginatedStages = useMemo(() => filteredStages.slice(
(stagePage - 1) * pageSize,
stagePage * pageSize
), [filteredStages, stagePage, pageSize])
const hasMorePages = useMemo(() => stagePage * pageSize < filteredStages.length, [stagePage, filteredStages])
useEffect(() => {
setStagePage(1)
}, [stageSearchTerm])
return (
<div className="container mx-auto p-6 z-4" onClick={() => {
setShowSearchWaveId(null)
setShowSearchStage(false)
}}>
<div className="mb-4 w-full relative">
<div className="flex items-center justify-center gap-2">
<Search className="w-6 h-6" />
<button
className="btn btn-outline w-[95%] text-left"
onClick={(e) => {
e.stopPropagation()
setShowSearchStage(true)
}}
>
<span className="text-left"> {transI18n("stage")}: {stageList.find((s) => s.id === ce_config.stage_id.toString())?.name || transI18n("selectStage")}</span>
</button>
</div>
{showSearchStage && (
<div className="absolute top-full mt-2 w-full z-50 border bg-base-200 border-slate-600 rounded-lg p-4 shadow-lg">
<div className="flex items-center gap-2 mb-2">
<Search className="w-4 h-4 text-slate-400" />
<input
type="text"
placeholder={transI18n("searchStage")}
className="input input-sm w-full placeholder-slate-400"
value={stageSearchTerm}
onChange={(e) => setStageSearchTerm(e.target.value)}
autoFocus
/>
</div>
<div className="max-h-60 overflow-y-auto space-y-1">
{paginatedStages.length > 0 ? (
<>
{paginatedStages.map((stage) => (
<div
key={stage.id}
className="p-2 hover:bg-primary/20 rounded cursor-pointer"
onClick={() => {
setCeConfig({ ...ce_config, stage_id: Number(stage.id), cycle_count: 30 })
setShowSearchStage(false)
setStageSearchTerm("")
}}
>
{stage.name}
</div>
))}
</>
) : (
<div className="text-sm text-center py-4">{transI18n("noStageFound")}</div>
)}
</div>
{paginatedStages.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<button
disabled={stagePage === 1}
className="btn btn-sm btn-outline btn-success mt-2"
onClick={(e) => {
e.stopPropagation()
setStagePage(stagePage - 1)
}}
>
{transI18n("previous")}
</button>
<button
disabled={!hasMorePages}
className="btn btn-sm btn-outline btn-success mt-2"
onClick={(e) => {
e.stopPropagation()
setStagePage(stagePage + 1)
}}
>
{transI18n("next")}
</button>
</div>
)}
</div>
)}
</div>
<div className="relative max-w-7xl mx-auto space-y-6">
{ce_config.monsters.map((wave, waveIndex) => (
<div key={waveIndex} className="card border border-slate-700/50 ">
<div className="card-body p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-white">{transI18n("wave")} {waveIndex + 1}</h2>
<div className="flex gap-2">
<button
disabled={waveIndex === 0}
onClick={(e) => {
e.stopPropagation()
const newCeConfig = cloneDeep(ce_config)
const waves = newCeConfig.monsters
const temp = waves[waveIndex - 1]
waves[waveIndex - 1] = waves[waveIndex]
waves[waveIndex] = temp
setCeConfig(newCeConfig)
}}
className="btn btn-sm btn-success"
>
<ChevronUp className="w-4 h-4" />
</button>
<button
disabled={waveIndex === ce_config.monsters.length - 1}
onClick={(e) => {
e.stopPropagation()
const newCeConfig = cloneDeep(ce_config)
const waves = newCeConfig.monsters
const temp = waves[waveIndex + 1]
waves[waveIndex + 1] = waves[waveIndex]
waves[waveIndex] = temp
setCeConfig(newCeConfig)
}}
className="btn btn-sm btn-success">
<ChevronDown className="w-4 h-4" />
</button>
<button
onClick={(e) => {
e.stopPropagation()
const newCeConfig = cloneDeep(ce_config)
newCeConfig.monsters.splice(waveIndex, 1)
setCeConfig(newCeConfig)
}}
className="btn btn-sm btn-error">
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4 w-full h-full">
{wave.map((member, memberIndex) => (
<div key={memberIndex} className="relative group z-7 w-full h-full">
<div className="card border hover:border-slate-500 transition-colors w-full h-full">
<div className="card-body p-4">
<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)
newCeConfig.monsters[waveIndex].splice(memberIndex, 1)
setCeConfig(newCeConfig)
}}
>
<Trash2 className="w-3 h-3" />
</button>
<div className="flex justify-center">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster2) => monster2.child.includes(member.monster_id))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className=" object-contain w-20 h-20 overflow-hidden"
/>
</div>
<div className="flex justify-center gap-1 mb-2">
{listMonster
.find((monster) => monster.child.includes(member.monster_id))
?.weak?.map((icon, iconIndex) => (
<Image
src={`/icon/${icon.toLowerCase()}.webp`}
alt={icon}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md border border-white/20 shadow-sm"
width={200}
height={200}
key={iconIndex}
/>
))}
</div>
<div className="text-center flex flex-col items-center justify-center">
<div className="text-sm font-medium">
{getLocaleName(locale, listMonster.find((monster) => monster.child.includes(member.monster_id)))}
</div>
<div className="flex items-center gap-1 mt-1">
<span className="text-sm">Lv.</span>
<input
type="number"
className="w-16 text-center input input-sm"
value={member.level}
onChange={(e) => {
try {
Number(e.target.value)
} catch {
return;
}
if (Number(e.target.value) < 1 || Number(e.target.value) > 95) return;
const newCeConfig = cloneDeep(ce_config)
newCeConfig.monsters[waveIndex][memberIndex].level = Number(e.target.value)
setCeConfig(newCeConfig)
}}
/>
</div>
</div>
</div>
</div>
</div>
))}
{/* Add Member Button + Search */}
<div className="relative flex items-start justify-center w-full h-full">
<button
className="btn btn-outline btn-primary w-full h-full border-dashed"
onClick={(e) => {
e.stopPropagation();
setShowSearchWaveId(waveIndex)
}}
>
<Plus className="w-8 h-8" />
</button>
{showSearchWaveId === waveIndex && (
<div className="absolute top-full mt-2 w-72 z-50 border bg-base-200 border-slate-600 rounded-lg p-4 shadow-lg">
<div className="flex items-center gap-2 mb-2">
<Search className="w-4 h-4" />
<input
type="text"
placeholder={transI18n("searchMonster")}
className="input input-sm w-full placeholder-slate-400"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
autoFocus
/>
</div>
<div className="max-h-60 overflow-y-auto space-y-1">
{filteredMonsters.length > 0 ? (
filteredMonsters.map((monster) => (
<div
key={monster.id}
className="flex items-center gap-2 p-2 hover:bg-success/40 rounded cursor-pointer"
onClick={() => {
const newCeConfig = cloneDeep(ce_config)
newCeConfig.monsters[waveIndex].push({
monster_id: Number(monster.id),
level: 95,
amount: 1,
})
setCeConfig(newCeConfig)
setShowSearchWaveId(null);
}}
>
<div className="relative w-8 h-8 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster2) => monster2.child.includes(Number(monster.id)))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
</div>
<span className="">{getLocaleName(locale, monster)} {`(${monster.id})`}</span>
</div>
))
) : (
<div className="text-sm text-center py-4">
{transI18n("noMonstersFound")}
</div>
)}
</div>
</div>
)}
</div>
</div>
</div>
</div>
))}
{/* Add New Wave Button */}
<div className="card backdrop-blur-sm border border-slate-700/50 border-dashed">
<div className="card-body p-8">
<button
onClick={() => {
const newCeConfig = cloneDeep(ce_config)
newCeConfig.monsters.push([])
setCeConfig(newCeConfig)
}}
className="btn btn-primary btn-lg w-full">
<Plus className="w-6 h-6 mr-2" />
{transI18n("addNewWave")}
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,87 @@
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";
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("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">
<div className="container mx-auto px-4">
<div className="flex items-center justify-center h-16">
{/* Navigation Tabs */}
<div className="flex space-x-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 src={`/icon/${item.icon}.webp`} alt={item.name} width={24} height={24} />
</span>
<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
}
>
<span className="font-medium">{item.name}</span>
</span>
</button>
))}
</div>
</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() === 'SU' && (
<div className="container mx-auto px-4 py-8 text-center font-bold text-3xl">
{transI18n("comingSoon")}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,321 @@
"use client"
import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText";
import useEventStore from "@/stores/eventStore";
import { getLocaleName, replaceByParam } from "@/helper";
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";
export default function MocBar() {
const { MOCEvent, mapMOCInfo } = useEventStore()
const { listMonster } = useMonsterStore()
const { locale } = useLocaleStore()
const {
moc_config,
setMocConfig
} = useUserDataStore()
const { MOC } = useMazeStore()
const transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => {
return mapMOCInfo[moc_config.event_id.toString()]?.find((moc) => moc.Id === moc_config.challenge_id)
}, [moc_config, mapMOCInfo])
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)
newBattleConfig.cycle_count = 0
if (moc_config.use_cycle_count) {
newBattleConfig.cycle_count = challenge.Countdown
}
newBattleConfig.blessings = []
if (moc_config.use_turbulence_buff && MOC[moc_config.challenge_id.toString()]) {
newBattleConfig.blessings.push({
id: Number(MOC[moc_config.challenge_id.toString()].maze_buff),
level: 1
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = 0
if ((moc_config.floor_side === "Upper" || moc_config.floor_side === "Upper -> Lower") && challenge.EventIDList1.length > 0) {
newBattleConfig.stage_id = challenge.EventIDList1[0].StageID
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if ((moc_config.floor_side === "Lower" || moc_config.floor_side === "Lower -> Upper") && challenge.EventIDList2.length > 0) {
newBattleConfig.stage_id = challenge.EventIDList2[0].StageID
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if (moc_config.floor_side === "Lower -> Upper" && challenge.EventIDList1.length > 0) {
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
} else if (moc_config.floor_side === "Upper -> Lower" && challenge.EventIDList2.length > 0) {
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
setMocConfig(newBattleConfig)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
moc_config.event_id,
moc_config.challenge_id,
moc_config.floor_side,
mapMOCInfo,
MOC,
moc_config.use_cycle_count,
moc_config.use_turbulence_buff,
])
return (
<div className="container mx-auto px-4 py-8 relative">
{/* Title Card */}
<div className="rounded-xl p-4 mb-2 border border-warning">
<div className="mb-4 w-full">
<SelectCustomText
customSet={MOCEvent.map((moc) => ({
id: moc.id,
name: getLocaleName(locale, moc),
time: `${moc.begin} - ${moc.end}`,
}))}
excludeSet={[]}
selectedCustomSet={moc_config.event_id.toString()}
placeholder={transI18n("selectMOCEvent")}
setSelectedCustomSet={(id) => setMocConfig({ ...moc_config, event_id: Number(id), challenge_id: 0 })}
/>
</div>
{/* Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<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>
{mapMOCInfo[moc_config.event_id.toString()]?.map((moc) => (
<option key={moc.Id} value={moc.Id}>{moc.Id % 100}</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>
<option value="Upper">{transI18n("upper")}</option>
<option value="Lower">{transI18n("lower")}</option>
<option value="Upper -> Lower">{transI18n("upperToLower")}</option>
<option value="Lower -> Upper">{transI18n("lowerToUpper")}</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>
{/* 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?.Desc ? (
<div
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
challengeSelected?.Desc || "",
challengeSelected?.Param || []
)
}}
/>
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* First Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("firstHalfEnemies")}</h2>
{challengeSelected && challengeSelected?.EventIDList1?.length > 0 && challengeSelected?.EventIDList1[0].MonsterList.map((wave, waveIndex) => (
<div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold mb-t">{transI18n("wave")} {waveIndex + 1}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Object.values(wave).map((waveValue, enemyIndex) => (
<div
key={enemyIndex}
className="rounded-xl p-2 border border-white/10 shadow-md hover:border-white/20 hover:shadow-lg transition"
>
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(waveValue))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
</div>
<div className="flex flex-col">
<div className="text-sm font-semibold">Lv. {challengeSelected?.EventIDList1[0].Level}</div>
<div className="flex items-center space-x-1 mt-1">
{listMonster
.find((monster) => monster.child.includes(waveValue))
?.weak?.map((icon, iconIndex) => (
<Image
src={`/icon/${icon.toLowerCase()}.webp`}
alt={icon}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md border border-white/20 shadow-sm"
width={200}
height={200}
key={iconIndex}
/>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
{/* Second Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("secondHalfEnemies")}</h2>
{challengeSelected && challengeSelected?.EventIDList2?.length > 0 && challengeSelected?.EventIDList2[0].MonsterList.map((wave, waveIndex) => (
<div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold mb-t">{transI18n("wave")} {waveIndex + 1}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Object.values(wave).map((waveValue, enemyIndex) => (
<div
key={enemyIndex}
className="rounded-xl p-2 border border-white/10 shadow-md hover:border-white/20 hover:shadow-lg transition"
>
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(waveValue))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
</div>
<div className="flex flex-col">
<div className="text-sm font-semibold">Lv. {challengeSelected?.EventIDList1[0].Level}</div>
<div className="flex items-center space-x-1 mt-1">
{listMonster
.find((monster) => monster.child.includes(waveValue))
?.weak?.map((icon, iconIndex) => (
<Image
src={`/icon/${icon.toLowerCase()}.webp`}
alt={icon}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md border border-white/20 shadow-sm"
width={200}
height={200}
key={iconIndex}
/>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,330 @@
"use client"
import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText";
import useEventStore from "@/stores/eventStore";
import { getLocaleName, replaceByParam } from "@/helper";
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";
export default function PfBar() {
const { PFEvent, mapPFInfo } = useEventStore()
const { listMonster } = useMonsterStore()
const { locale } = useLocaleStore()
const {
pf_config,
setPfConfig
} = useUserDataStore()
const { PF } = useMazeStore()
const transI18n = useTranslations("DataPage")
const challengeSelected = useMemo(() => {
return mapPFInfo[pf_config.event_id.toString()]?.Level.find((pf) => pf.Id === pf_config.challenge_id)
}, [pf_config, mapPFInfo])
const eventSelected = useMemo(() => {
return mapPFInfo[pf_config.event_id.toString()]
}, [pf_config, mapPFInfo])
useEffect(() => {
const challenge = mapPFInfo[pf_config.event_id.toString()]?.Level.find((pf) => pf.Id === pf_config.challenge_id)
if (pf_config.event_id !== 0 && pf_config.challenge_id !== 0 && challenge) {
const newBattleConfig = cloneDeep(pf_config)
newBattleConfig.cycle_count = 0
newBattleConfig.blessings = []
if (pf_config.buff_id !== 0) {
newBattleConfig.blessings.push({
id: pf_config.buff_id,
level: 1
})
}
if (PF[pf_config.challenge_id.toString()]) {
newBattleConfig.blessings.push({
id: Number(PF[pf_config.challenge_id.toString()].maze_buff),
level: 1
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = 0
if ((pf_config.floor_side === "Upper" || pf_config.floor_side === "Upper -> Lower" ) && challenge.EventIDList1.length > 0 ) {
newBattleConfig.stage_id = challenge.EventIDList1[0].StageID
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if ((pf_config.floor_side === "Lower" || pf_config.floor_side === "Lower -> Upper") && challenge.EventIDList2.length > 0) {
newBattleConfig.stage_id = challenge.EventIDList2[0].StageID
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
if (pf_config.floor_side === "Lower -> Upper" && challenge.EventIDList1.length > 0) {
for (const wave of challenge.EventIDList1[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList1[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
} else if (pf_config.floor_side === "Upper -> Lower" && challenge.EventIDList2.length > 0) {
for (const wave of challenge.EventIDList2[0].MonsterList) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
newWave.push({
monster_id: Number(value),
level: challenge.EventIDList2[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
}
setPfConfig(newBattleConfig)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
pf_config.event_id,
pf_config.challenge_id,
pf_config.floor_side,
pf_config.buff_id,
mapPFInfo,
PF,
])
return (
<div className="container mx-auto px-4 py-8 relative">
{/* Title Card */}
<div className="rounded-xl p-4 mb-2 border border-warning">
<div className="mb-4 w-full">
<SelectCustomText
customSet={PFEvent.map((pf) => ({
id: pf.id,
name: getLocaleName(locale, pf),
time: `${pf.begin} - ${pf.end}`,
}))}
excludeSet={[]}
selectedCustomSet={pf_config.event_id.toString()}
placeholder={transI18n("selectPFEvent")}
setSelectedCustomSet={(id) => setPfConfig({ ...pf_config, event_id: Number(id), challenge_id: 0, buff_id: 0 })}
/>
</div>
{/* Settings */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
<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>
{mapPFInfo[pf_config.event_id.toString()]?.Level.map((pf) => (
<option key={pf.Id} value={pf.Id}>{pf.Id % 10}</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>
<option value="Upper">{transI18n("upper")}</option>
<option value="Lower">{transI18n("lower")}</option>
<option value="Upper -> Lower">{transI18n("upperToLower")}</option>
<option value="Lower -> Upper">{transI18n("lowerToUpper")}</option>
</select>
</div>
</div>
{eventSelected && (
<div className="mb-4 w-full">
<SelectCustomText
customSet={eventSelected.Option.map((buff, index) => ({
id: PF[eventSelected.Id.toString()]?.buff[index]?.toString(),
name: buff.Name,
description: replaceByParam(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}. {subOption.Name}</span>
</label>
<div
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
subOption.Desc,
subOption.Param || []
)
}}
/>
</div>
))
) : eventSelected && eventSelected.SubOption.length === 0 ? (
<div
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
eventSelected.Buff?.Desc || "",
eventSelected.Buff?.Param || []
)
}}
/>
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* First Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("firstHalfEnemies")}</h2>
{challengeSelected && Object.values(challengeSelected.InfiniteList1).map((waveValue, waveIndex) => (
<div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold mb-t">{transI18n("wave")} {waveIndex + 1}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Array.from(new Set(waveValue.MonsterGroupIDList)).map((monsterId, enemyIndex) => (
<div
key={enemyIndex}
className="rounded-xl p-2 border border-white/10 shadow-md hover:border-white/20 hover:shadow-lg transition"
>
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(monsterId))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
</div>
<div className="flex flex-col">
<div className="text-sm font-semibold">Lv. {challengeSelected?.EventIDList1[0].Level}</div>
<div className="flex items-center space-x-1 mt-1">
{listMonster
.find((monster) => monster.child.includes(monsterId))
?.weak?.map((icon, iconIndex) => (
<Image
src={`/icon/${icon.toLowerCase()}.webp`}
alt={icon}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md border border-white/20 shadow-sm"
width={200}
height={200}
key={iconIndex}
/>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
{/* Second Half */}
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-6 text-info">{transI18n("secondHalfEnemies")}</h2>
{challengeSelected && Object.values(challengeSelected?.InfiniteList2).map((waveValue, waveIndex) => (
<div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold mb-t">{transI18n("wave")} {waveIndex + 1}</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Array.from(new Set(waveValue.MonsterGroupIDList)).map((monsterId, enemyIndex) => (
<div
key={enemyIndex}
className="rounded-xl p-2 border border-white/10 shadow-md hover:border-white/20 hover:shadow-lg transition"
>
<div className="flex items-center space-x-3">
<div className="relative w-20 h-20 rounded-full overflow-hidden flex-shrink-0 border border-white/10 shadow-sm">
<Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listMonster.find((monster) => monster.child.includes(monsterId))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`}
alt="Enemy Icon"
width={376}
height={512}
className="w-full h-full object-cover"
/>
</div>
<div className="flex flex-col">
<div className="text-sm font-semibold">Lv. {challengeSelected?.EventIDList1[0].Level}</div>
<div className="flex items-center space-x-1 mt-1">
{listMonster
.find((monster) => monster.child.includes(monsterId))
?.weak?.map((icon, iconIndex) => (
<Image
src={`/icon/${icon.toLowerCase()}.webp`}
alt={icon}
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md border border-white/20 shadow-sm"
width={200}
height={200}
key={iconIndex}
/>
))}
</div>
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +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>
);
}

View File

@@ -3,7 +3,7 @@ import useRelicStore from '@/stores/relicStore';
import useUserDataStore from '@/stores/userDataStore';
import { AffixDetail, RelicDetail } from '@/types';
import React, { useEffect, useMemo } from 'react';
import SelectCustom from '../select';
import SelectCustomImage from '../select/customSelectImage';
import { calcAffixBonus, randomPartition, randomStep, replaceByParam } from '@/helper';
import useAffixStore from '@/stores/affixStore';
import { mapingStats } from '@/lib/constant';
@@ -12,6 +12,7 @@ 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'
export default function RelicMaker() {
const { avatars, setAvatars } = useUserDataStore()
@@ -77,7 +78,7 @@ export default function RelicMaker() {
const mainProp = mainAffixMap[selectedMainStat]?.property;
if (!mainProp) return;
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
let updated = false;
for (let i = 0; i < newSubAffixes.length; i++) {
@@ -91,6 +92,7 @@ export default function RelicMaker() {
}
if (updated) setListSelectedSubStats(newSubAffixes);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedMainStat, mapSubAffix, mapMainAffix, selectedRelicSlot]);
const exSubAffixOptions = useMemo(() => {
@@ -129,7 +131,7 @@ export default function RelicMaker() {
}, [mapMainAffix, selectedRelicSlot, selectedMainStat, selectedRelicLevel]);
const handleSubStatChange = (key: string, index: number, rollCount: number, stepCount: number) => {
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
if (!subAffixOptions[key]) {
newSubAffixes[index].affixId = "";
newSubAffixes[index].property = "";
@@ -153,8 +155,8 @@ export default function RelicMaker() {
const keys = Object.keys(preSelectedSubStats[index]);
if (keys.length <= 1) return;
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const listHistory = preSelectedSubStats[index].map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
const listHistory = cloneDeep(preSelectedSubStats[index]);
const secondLastKey = listHistory.length - 2;
const preSubAffixes = { ...listHistory[secondLastKey] };
newSubAffixes[index].rollCount = preSubAffixes.rollCount;
@@ -164,7 +166,7 @@ export default function RelicMaker() {
};
const resetSubStat = (index: number) => {
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
resetHistory(index);
newSubAffixes[index].affixId = "";
newSubAffixes[index].property = "";
@@ -174,7 +176,7 @@ export default function RelicMaker() {
};
const randomizeStats = () => {
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
const exKeys = Object.keys(exSubAffixOptions);
for (let i = 0; i < newSubAffixes.length; i++) {
const keys = Object.keys(subAffixOptions).filter((key) => !exKeys.includes(key));
@@ -194,7 +196,7 @@ export default function RelicMaker() {
};
const randomizeRolls = () => {
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
const newSubAffixes = cloneDeep(listSelectedSubStats);
const randomRolls = randomPartition(9, listSelectedSubStats.length);
for (let i = 0; i < listSelectedSubStats.length; i++) {
newSubAffixes[i].rollCount = randomRolls[i];
@@ -252,7 +254,7 @@ export default function RelicMaker() {
{/* Main Stat */}
<div>
<label className="block text-lg font-medium mb-2">{transI18n("mainStat")}</label>
<SelectCustom
<SelectCustomImage
customSet={Object.entries(mapMainAffix["5" + selectedRelicSlot] || {}).map(([key, value]) => ({
value: key,
label: mapingStats[value.property].name + " " + mapingStats[value.property].unit,
@@ -267,7 +269,7 @@ export default function RelicMaker() {
{/* Relic Set Selection */}
<div>
<label className="block text-lg font-medium mb-2">{transI18n("set")}</label>
<SelectCustom
<SelectCustomImage
customSet={Object.entries(relicSets).map(([key, value]) => ({
value: key,
label: value.Name,
@@ -356,7 +358,7 @@ export default function RelicMaker() {
{/* Stat Selection */}
<div className="col-span-8">
<SelectCustom
<SelectCustomImage
customSet={Object.entries(subAffixOptions).map(([key, value]) => ({
value: key,
label: mapingStats[value.property].name + " " + mapingStats[value.property].unit,

View File

@@ -1,11 +1,8 @@
"use client"
;
import { useRouter } from 'next/navigation'
import { useCallback, useEffect, useMemo, useState } from "react";
"use client";
import { useCallback, useEffect, useMemo } from "react";
import RelicMaker from "../relicBar";
import { motion } from "framer-motion";
import useUserDataStore from "@/stores/userDataStore";
import useLocaleStore from "@/stores/localeStore";
import { useTranslations } from "next-intl";
import RelicCard from "../card/relicCard";
import useAvatarStore from "@/stores/avatarStore";
@@ -16,7 +13,6 @@ import useRelicMakerStore from '@/stores/relicMakerStore';
import useAffixStore from '@/stores/affixStore';
export default function RelicsInfo() {
const router = useRouter()
const { avatars, setAvatars } = useUserDataStore()
const { avatarSelected } = useAvatarStore()
const {
@@ -32,7 +28,7 @@ export default function RelicsInfo() {
const { mapSubAffix } = useAffixStore()
const { isOpenRelic, setIsOpenRelic } = useModelStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore();
const { mapRelicInfo } = useRelicStore()
const handleShow = (modalId: string) => {
@@ -67,6 +63,7 @@ export default function RelicsInfo() {
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpenRelic]);
const getRelic = useCallback((slot: string) => {
@@ -124,7 +121,7 @@ export default function RelicsInfo() {
const avatar = avatars[avatarSelected?.id || ""];
const relicCount: { [key: string]: number } = {};
if (avatar) {
for (const [slot, relic] of Object.entries(avatar.profileList[avatar.profileSelect].relics)) {
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
if (relicCount[relic.relic_set_id]) {
relicCount[relic.relic_set_id]++;
} else {
@@ -152,7 +149,7 @@ export default function RelicsInfo() {
<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>
Equipment
{transI18n("relics")}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-2xl mx-auto">
@@ -170,16 +167,14 @@ export default function RelicsInfo() {
/>
</div>
{/* Action buttons - chỉ hiện khi hover */}
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
{/* Nút chuyển relic */}
<button
onClick={(e) => {
e.stopPropagation()
handlerChangeRelic(item)
}}
className="btn btn-info p-1.5 rounded-full shadow-lg transition-colors duration-200"
title="Chuyển relic"
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" />
@@ -190,12 +185,12 @@ export default function RelicsInfo() {
<button
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`Bạn có chắc muốn xóa relic ở slot ${item}?`)) {
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${item}?`)) {
handlerDeleteRelic(item)
}
}}
className="btn btn-error p-1.5 rounded-full shadow-lg transition-colors duration-200"
title="Xóa relic"
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" />
@@ -216,7 +211,7 @@ export default function RelicsInfo() {
<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-gradient-to-b from-primary to-primary/50 rounded-full"></div>
Set Effects
{transI18n("setEffects")}
</h3>
<div className="space-y-6">

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import Select, { SingleValue } from 'react-select'
import Image from 'next/image'
@@ -18,7 +19,7 @@ type SelectCustomProp = {
setSelectedCustomSet: (value: string) => void
}
export default function SelectCustom({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
export default function SelectCustomImage({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const options: SelectOption[] = customSet
const { locale } = useLocaleStore()
const customStyles = {
@@ -27,22 +28,24 @@ export default function SelectCustom({ customSet, excludeSet, selectedCustomSet,
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px'
padding: '8px',
backgroundColor: 'transparent',
}),
singleValue: (provided: any) => ({
...provided,
display: 'flex',
alignItems: 'center',
gap: '8px'
gap: '8px',
backgroundColor: 'transparent',
}),
menuPortal: (provided: any) => ({ ...provided, zIndex: 9999 }),
menu: (provided: any) => ({ ...provided, zIndex: 9999 })
}
const formatOptionLabel = (option: SelectOption) => (
<div className="flex items-center gap-1 bg-slate-400 w-full h-full z-50">
<Image src={option.imageUrl} alt="" width={125} height={125} className="w-8 h-8 object-contain" />
<ParseText className='text-black font-bold' text={option.label} locale={locale} />
<div className="flex items-center gap-1 w-full h-full z-50">
<Image 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-warning-content' text={option.label} locale={locale} />
</div>
)
@@ -52,7 +55,6 @@ export default function SelectCustom({ customSet, excludeSet, selectedCustomSet,
value={options.find(opt => {
return opt.value === selectedCustomSet}) || null}
onChange={(selected: SingleValue<SelectOption>) => {
console.log(selected);
setSelectedCustomSet(selected?.value || '')
}}
formatOptionLabel={formatOptionLabel}

View File

@@ -0,0 +1,83 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import Select, { SingleValue } from 'react-select'
import { replaceByParam } from '@/helper'
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 customStyles = {
option: (provided: any) => ({
...provided,
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px',
backgroundColor: 'transparent',
}),
singleValue: (provided: any) => ({
...provided,
display: 'flex',
alignItems: 'center',
gap: '8px'
}),
menuPortal: (provided: any) => ({ ...provided, zIndex: 9999 }),
menu: (provided: any) => ({ ...provided, zIndex: 9999 })
}
const formatOptionLabel = (option: SelectOption) => (
<div className="flex flex-col gap-1 w-full h-full z-50 text-warning-content ">
<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
/>
)
}