UPDATE: monster bar
Some checks failed
Gitea Auto Deploy / Deploy-Container (push) Failing after 1m36s
Some checks failed
Gitea Auto Deploy / Deploy-Container (push) Failing after 1m36s
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useRef} from 'react';
|
||||
|
||||
export default function ShowCaseInfo() {
|
||||
|
||||
@@ -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 } } })
|
||||
}}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
338
src/components/monsterBar/as.tsx
Normal file
338
src/components/monsterBar/as.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
364
src/components/monsterBar/ce.tsx
Normal file
364
src/components/monsterBar/ce.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
87
src/components/monsterBar/index.tsx
Normal file
87
src/components/monsterBar/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
321
src/components/monsterBar/moc.tsx
Normal file
321
src/components/monsterBar/moc.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
330
src/components/monsterBar/pf.tsx
Normal file
330
src/components/monsterBar/pf.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
12
src/components/queryProvider/index.tsx
Normal file
12
src/components/queryProvider/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
83
src/components/select/customSelectText.tsx
Normal file
83
src/components/select/customSelectText.tsx
Normal 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
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user