All checks were successful
Gitea Auto Deploy / Deploy-Container (push) Successful in 1m20s
534 lines
30 KiB
TypeScript
534 lines
30 KiB
TypeScript
"use client";
|
|
import { useEffect, useMemo, useState } from "react";
|
|
import {
|
|
Plus,
|
|
Trash2,
|
|
ChevronUp,
|
|
ChevronDown,
|
|
Search,
|
|
CopyPlus,
|
|
} from "lucide-react";
|
|
|
|
import useUserDataStore from "@/stores/userDataStore";
|
|
import useLocaleStore from "@/stores/localeStore";
|
|
import { getLocaleName } from "@/helper";
|
|
import Image from "next/image";
|
|
import { useTranslations } from "next-intl";
|
|
import useGlobalStore from "@/stores/globalStore";
|
|
import useDetailDataStore from "@/stores/detailDataStore";
|
|
import { MonsterDetail } from "@/types";
|
|
|
|
|
|
export default function CeBar() {
|
|
const [searchTerm, setSearchTerm] = useState("");
|
|
const [showSearchWaveId, setShowSearchWaveId] = useState<number | null>(null);
|
|
const { ce_config, setCeConfig } = useUserDataStore()
|
|
const { mapMonster, stage, damageType } = useDetailDataStore()
|
|
const { locale } = useLocaleStore()
|
|
const transI18n = useTranslations("DataPage")
|
|
const [showSearchStage, setShowSearchStage] = useState(false)
|
|
const [stageSearchTerm, setStageSearchTerm] = useState("")
|
|
const [stagePage, setStagePage] = useState(1)
|
|
const { extraData, setExtraData } = useGlobalStore()
|
|
|
|
const pageSize = 30
|
|
|
|
const pageSizeMonsters = 30
|
|
const [monsterPage, setMonsterPage] = useState(1)
|
|
|
|
const filteredMonsters = useMemo(() => {
|
|
const newlistMonster = new Set<MonsterDetail>()
|
|
for (const monster of Object.values(mapMonster)) {
|
|
if (getLocaleName(locale, monster.Name).toLowerCase().includes(searchTerm.toLowerCase())) {
|
|
newlistMonster.add(monster)
|
|
}
|
|
if (monster.ID.toString().includes(searchTerm.toLowerCase())) {
|
|
newlistMonster.add(monster)
|
|
}
|
|
}
|
|
return Array.from(newlistMonster)
|
|
}, [locale, searchTerm, mapMonster]);
|
|
|
|
const paginatedMonsters = useMemo(() =>
|
|
filteredMonsters.slice((monsterPage - 1) * pageSizeMonsters, monsterPage * pageSizeMonsters),
|
|
[filteredMonsters, monsterPage]
|
|
)
|
|
|
|
const hasMoreMonsterPages = useMemo(
|
|
() => monsterPage * pageSizeMonsters < filteredMonsters.length,
|
|
[monsterPage, filteredMonsters]
|
|
)
|
|
|
|
useEffect(() => {
|
|
setMonsterPage(1)
|
|
}, [searchTerm])
|
|
|
|
const stageList = useMemo(() => Object.values(stage).map((item) => ({
|
|
id: item.ID.toString(),
|
|
name: `${getLocaleName(locale, item.Name)} (${item.ID})`,
|
|
})), [stage, locale])
|
|
|
|
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])
|
|
|
|
|
|
useEffect(() => {
|
|
if (!ce_config) return
|
|
if (!extraData || !extraData.theory_craft?.mode) return
|
|
|
|
const newExtraData = structuredClone(extraData)
|
|
if (!newExtraData?.theory_craft?.hp) {
|
|
newExtraData.theory_craft!.hp = {}
|
|
}
|
|
|
|
for (let i = 0; i < ce_config.monsters.length; i++) {
|
|
const waveKey = (i + 1).toString()
|
|
if (!newExtraData.theory_craft!.hp[waveKey]) {
|
|
newExtraData.theory_craft!.hp[waveKey] = []
|
|
}
|
|
for (let j = 0; j < ce_config.monsters[i].length; j++) {
|
|
if (newExtraData.theory_craft!.hp[waveKey][j] === undefined) {
|
|
newExtraData.theory_craft!.hp[waveKey][j] = 0
|
|
}
|
|
}
|
|
}
|
|
setExtraData(newExtraData)
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [ce_config])
|
|
|
|
|
|
return (
|
|
<div className="z-4 py-8 h-full w-full" onClick={() => {
|
|
|
|
setShowSearchWaveId(null)
|
|
setShowSearchStage(false)
|
|
}}>
|
|
|
|
<div className="mb-4 w-full relative">
|
|
<div className="flex items-center justify-center gap-2">
|
|
<button
|
|
className="btn btn-outline w-full text-left flex items-center gap-2"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
setShowSearchStage(!showSearchStage)
|
|
}}
|
|
>
|
|
<Search className="w-6 h-6" />
|
|
<span className="text-left"> {transI18n("stage")}: {stageList.find((s) => s.id === ce_config.stage_id.toString())?.name || transI18n("selectStage")}</span>
|
|
</button>
|
|
</div>
|
|
{showSearchStage && (
|
|
<div onClick={(e) => e.stopPropagation()} 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">
|
|
|
|
<label className="input w-full">
|
|
<svg className="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<g
|
|
strokeLinejoin="round"
|
|
strokeLinecap="round"
|
|
strokeWidth="2.5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
>
|
|
<circle cx="11" cy="11" r="8"></circle>
|
|
<path d="m21 21-4.3-4.3"></path>
|
|
</g>
|
|
</svg>
|
|
<input
|
|
type="search" className="grow"
|
|
placeholder={transI18n("searchStage")}
|
|
value={stageSearchTerm}
|
|
onChange={(e) => setStageSearchTerm(e.target.value)}
|
|
autoFocus
|
|
/>
|
|
|
|
</label>
|
|
</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={() => {
|
|
if (ce_config.stage_id !== Number(stage.id)) {
|
|
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 flex-wrap 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 = structuredClone(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 = structuredClone(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 = structuredClone(ce_config)
|
|
const waves = newCeConfig.monsters
|
|
const temp = waves[waveIndex]
|
|
newCeConfig.monsters.push([...temp])
|
|
setCeConfig(newCeConfig)
|
|
}}
|
|
className="btn btn-sm btn-success">
|
|
<CopyPlus className="w-4 h-4" />
|
|
</button>
|
|
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
const newCeConfig = structuredClone(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-success absolute -top-2 right-12 opacity-50 group-hover:opacity-100 transition-opacity"
|
|
onClick={() => {
|
|
const newCeConfig = structuredClone(ce_config)
|
|
|
|
newCeConfig.monsters[waveIndex].push({
|
|
monster_id: Number(member.monster_id),
|
|
level: member.level,
|
|
amount: member.amount,
|
|
})
|
|
setCeConfig(newCeConfig)
|
|
}}
|
|
>
|
|
<CopyPlus className="w-3 h-3" />
|
|
</button>
|
|
<button
|
|
className="btn btn-xs btn-error absolute -top-2 right-2 opacity-50 group-hover:opacity-100 transition-opacity"
|
|
onClick={() => {
|
|
const newCeConfig = structuredClone(ce_config)
|
|
newCeConfig.monsters[waveIndex].splice(memberIndex, 1)
|
|
setCeConfig(newCeConfig)
|
|
}}
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
</button>
|
|
|
|
<div className="flex justify-center">
|
|
{mapMonster?.[member.monster_id.toString()]?.Image?.IconPath && <Image
|
|
unoptimized
|
|
crossOrigin="anonymous"
|
|
src={`${process.env.CDN_URL}/${mapMonster?.[member.monster_id.toString()]?.Image?.IconPath}`}
|
|
alt="Enemy Icon"
|
|
width={376}
|
|
height={512}
|
|
className=" object-contain w-20 h-20 overflow-hidden"
|
|
/>}
|
|
</div>
|
|
|
|
<div className="flex flex-wrap justify-center gap-1 mb-2">
|
|
{mapMonster?.[member.monster_id.toString()]?.StanceWeakList?.map((icon, iconIndex) => (
|
|
<Image
|
|
unoptimized
|
|
crossOrigin="anonymous"
|
|
src={`${process.env.CDN_URL}/${damageType[icon]?.Icon}`}
|
|
alt={icon}
|
|
className="h-7 w-7 2xl:h-10 2xl:w-10 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, mapMonster?.[member.monster_id.toString()]?.Name)} {`(${member.monster_id})`}
|
|
</div>
|
|
<div className="flex items-center gap-1 mt-1 mx-2">
|
|
<span className="text-sm">LV.</span>
|
|
<input
|
|
type="number"
|
|
className="text-center input input-sm"
|
|
value={member.level}
|
|
|
|
onChange={(e) => {
|
|
const val = Number(e.target.value)
|
|
if (isNaN(val) || val < 1 || val > 95) return
|
|
if (ce_config.monsters[waveIndex][memberIndex].level === val) return
|
|
|
|
const newCeConfig = structuredClone(ce_config)
|
|
newCeConfig.monsters[waveIndex][memberIndex].level = val
|
|
setCeConfig(newCeConfig)
|
|
}}
|
|
/>
|
|
</div>
|
|
{(extraData?.theory_craft?.mode === true && (
|
|
<div className="flex items-center gap-1 mt-1 mx-2">
|
|
<span className="text-sm">HP</span>
|
|
<input
|
|
type="number"
|
|
className="text-center input input-sm"
|
|
value={extraData?.theory_craft?.hp?.[(waveIndex + 1).toString()]?.[memberIndex] || 0}
|
|
|
|
onChange={(e) => {
|
|
const val = Number(e.target.value)
|
|
if (isNaN(val) || val < 0) return
|
|
|
|
const newData = structuredClone(extraData)
|
|
|
|
if (!newData?.theory_craft?.hp?.[(waveIndex + 1).toString()]) {
|
|
newData.theory_craft!.hp![(waveIndex + 1).toString()] = []
|
|
}
|
|
|
|
newData.theory_craft!.hp![(waveIndex + 1).toString()][memberIndex] = val
|
|
|
|
setExtraData(newData)
|
|
}}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{/* Add Member Button + Search */}
|
|
<div
|
|
className="relative flex items-start justify-center w-full h-full"
|
|
onClick={(e) => e.stopPropagation()}
|
|
>
|
|
<button
|
|
className="btn btn-outline btn-primary w-full h-full border-dashed"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
if (showSearchWaveId === waveIndex) {
|
|
setShowSearchWaveId(null)
|
|
return
|
|
}
|
|
|
|
setShowSearchWaveId(waveIndex)
|
|
}}
|
|
>
|
|
<Plus className="w-8 h-8" />
|
|
</button>
|
|
|
|
{showSearchWaveId === waveIndex && (
|
|
<div className="absolute top-full mt-2 w-72 border bg-base-200 border-slate-600 rounded-lg p-4 shadow-lg z-50">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
|
|
<label className="input w-full">
|
|
<svg className="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
<g
|
|
strokeLinejoin="round"
|
|
strokeLinecap="round"
|
|
strokeWidth="2.5"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
>
|
|
<circle cx="11" cy="11" r="8"></circle>
|
|
<path d="m21 21-4.3-4.3"></path>
|
|
</g>
|
|
</svg>
|
|
<input
|
|
type="search" className="grow"
|
|
placeholder={transI18n("searchMonster")}
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
autoFocus
|
|
/>
|
|
|
|
</label>
|
|
|
|
</div>
|
|
|
|
<div className="max-h-60 overflow-y-auto space-y-1">
|
|
{paginatedMonsters.length > 0 ? (
|
|
paginatedMonsters.map((monster) => (
|
|
<div
|
|
key={monster.ID}
|
|
className="flex items-center gap-2 p-2 hover:bg-success/40 rounded cursor-pointer"
|
|
onClick={() => {
|
|
const newCeConfig = structuredClone(ce_config)
|
|
newCeConfig.monsters[waveIndex].push({
|
|
monster_id: monster.ID,
|
|
level: 95,
|
|
amount: 1,
|
|
})
|
|
setCeConfig(newCeConfig)
|
|
setShowSearchWaveId(null)
|
|
}}
|
|
>
|
|
<div className="relative w-8 h-8 rounded-full overflow-hidden shrink-0 border border-white/10 shadow-sm">
|
|
{mapMonster?.[monster.ID.toString()]?.Image?.IconPath && (
|
|
<Image
|
|
unoptimized
|
|
crossOrigin="anonymous"
|
|
src={`${process.env.CDN_URL}/${mapMonster?.[monster.ID.toString()]?.Image?.IconPath}`}
|
|
alt="Enemy Icon"
|
|
width={376}
|
|
height={512}
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
)}
|
|
</div>
|
|
<span>{getLocaleName(locale, monster.Name)} {`(${monster.ID})`}</span>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="text-sm text-center py-4">
|
|
{transI18n("noMonstersFound")}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{filteredMonsters.length > 0 && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mt-2">
|
|
<button
|
|
disabled={monsterPage === 1}
|
|
className="btn btn-sm btn-outline btn-success"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
setMonsterPage(monsterPage - 1)
|
|
}}
|
|
>
|
|
{transI18n("previous")}
|
|
</button>
|
|
|
|
<button
|
|
disabled={!hasMoreMonsterPages}
|
|
className="btn btn-sm btn-outline btn-success"
|
|
onClick={(e) => {
|
|
e.stopPropagation()
|
|
setMonsterPage(monsterPage + 1)
|
|
}}
|
|
>
|
|
{transI18n("next")}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{/* Add New Wave Button */}
|
|
<div className="card border border-slate-700/50 border-dashed">
|
|
<div className="card-body p-8">
|
|
<button
|
|
onClick={() => {
|
|
const newCeConfig = structuredClone(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>
|
|
);
|
|
}
|