Refactor TypeScript files for improved readability and consistency
Gitea Auto Deploy / Deploy-Container (push) Successful in 3m21s
Gitea Auto Deploy / Deploy-Container (push) Successful in 3m21s
- Cleaned up imports and formatting in localeStore.ts and socketSettingStore.ts. - Standardized interface definitions and formatting in attack.ts, avatar.ts, battle.ts, enemy.ts, entity.ts, error.ts, index.ts, lineup.ts, mics.ts, skill.ts, srtools.ts, team.ts, turn.ts, version.ts, and waveAndCycle.ts. - Ensured consistent use of semicolons and spacing across all modified files. - Updated tsconfig.json for consistency in formatting.
This commit is contained in:
+282
-282
@@ -1,283 +1,283 @@
|
||||
"use client";
|
||||
import useAvatarDataStore from "@/stores/avatarDataStore";
|
||||
import useBattleDataStore from "@/stores/battleDataStore";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import { attackTypeToString, CharacterBasic } from "@/types";
|
||||
import { SkillBattleInfo } from "@/types/mics";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { motion } from "framer-motion";
|
||||
import { getNameChar } from "@/helper";
|
||||
import Image from "next/image";
|
||||
import NameAvatar from "../nameAvatar";
|
||||
|
||||
export default function ActionBar() {
|
||||
const [selectTurn, setSelectTurn] = useState<SkillBattleInfo | null>(null);
|
||||
const [selectAvatar, setSelectAvatar] = useState<CharacterBasic | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const { skillHistory, turnHistory, cycleIndex, waveIndex, maxWave } = useBattleDataStore();
|
||||
const { listAvatar } = useAvatarDataStore();
|
||||
const { locale } = useLocaleStore();
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const turnListRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
const contentStyle: React.CSSProperties = {
|
||||
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
const handleShow = (modalId: string, avatar: CharacterBasic, turn: SkillBattleInfo) => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
setSelectAvatar(avatar);
|
||||
setSelectTurn(turn);
|
||||
setIsModalOpen(true);
|
||||
modal.showModal();
|
||||
}
|
||||
};
|
||||
|
||||
// Close modal handler
|
||||
const handleCloseModal = (modalId: string) => {
|
||||
setIsModalOpen(false);
|
||||
setSelectAvatar(null);
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
modal.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle ESC key to close modal
|
||||
useEffect(() => {
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isModalOpen) {
|
||||
handleCloseModal("action_detail_modal");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleEscKey);
|
||||
return () => window.removeEventListener('keydown', handleEscKey);
|
||||
}, [isModalOpen]);
|
||||
|
||||
// Scroll to the bottom when new turns are added
|
||||
useEffect(() => {
|
||||
if (turnListRef.current && skillHistory.length > 0) {
|
||||
turnListRef.current.scrollTop = turnListRef.current.scrollHeight;
|
||||
}
|
||||
}, [skillHistory.length]);
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-1 rounded-lg shadow-lg w-full h-full">
|
||||
<motion.h2
|
||||
className="text-center text-xl lg:text-2xl mb-2 font-bold text-transparent bg-clip-text bg-linear-to-r from-pink-500 via-purple-500 to-cyan-500"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{transI18n("actionBar")}
|
||||
</motion.h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-1 xl:grid-cols-2 items-center justify-items-center w-full gap-2 mb-2 mx-2">
|
||||
<div className="badge badge-soft badge-info gap-2 text-lg py-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-6">
|
||||
<path fillRule="evenodd" d="M3 2.25a.75.75 0 0 1 .75.75v.54l1.838-.46a9.75 9.75 0 0 1 6.725.738l.108.054A8.25 8.25 0 0 0 18 4.524l3.11-.732a.75.75 0 0 1 .917.81 47.784 47.784 0 0 0 .005 10.337.75.75 0 0 1-.574.812l-3.114.733a9.75 9.75 0 0 1-6.594-.77l-.108-.054a8.25 8.25 0 0 0-5.69-.625l-2.202.55V21a.75.75 0 0 1-1.5 0V3A.75.75 0 0 1 3 2.25Z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span>{waveIndex}/{maxWave}</span>
|
||||
</div>
|
||||
<div className="badge badge-soft badge-error gap-2 text-lg py-3 items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" fill="currentColor" className="size-6">
|
||||
<path d="M0 32C0 14.3 14.3 0 32 0L64 0 320 0l32 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l0 11c0 42.4-16.9 83.1-46.9 113.1L237.3 256l67.9 67.9c30 30 46.9 70.7 46.9 113.1l0 11c17.7 0 32 14.3 32 32s-14.3 32-32 32l-32 0L64 512l-32 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l0-11c0-42.4 16.9-83.1 46.9-113.1L146.7 256 78.9 188.1C48.9 158.1 32 117.4 32 75l0-11C14.3 64 0 49.7 0 32zM96 64l0 11c0 25.5 10.1 49.9 28.1 67.9L192 210.7l67.9-67.9c18-18 28.1-42.4 28.1-67.9l0-11L96 64zm0 384l192 0 0-11c0-25.5-10.1-49.9-28.1-67.9L192 301.3l-67.9 67.9c-18 18-28.1 42.4-28.1 67.9l0 11z" />
|
||||
</svg>
|
||||
<span>{cycleIndex}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={turnListRef}
|
||||
className="flex md:block px-2 md:px-0 w-full pt-2 border-t-2 border-accent overflow-x-auto md:overflow-x-hidden md:overflow-y-auto max-h-[90vh] lg:max-h-[80vh]"
|
||||
>
|
||||
<div className="flex flex-nowrap md:grid md:grid-cols-1 gap-2 w-fit md:w-full">
|
||||
{skillHistory.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<p className="text-base-content opacity-50">{transI18n("noTurns")}</p>
|
||||
</div>
|
||||
) : (
|
||||
skillHistory.map((turn, index) => {
|
||||
const data = listAvatar.find(it => it.id === turn.avatarId.toString());
|
||||
if (!data) return null;
|
||||
const text = getNameChar(locale, transI18n, data);
|
||||
|
||||
return (
|
||||
<div key={index} className="h-full md:w-full">
|
||||
<div
|
||||
onClick={() => handleShow("action_detail_modal", data, turn)}
|
||||
className="h-full grid grid-cols-2 gap-2 border bg-base-100 w-full hover:bg-base-200 transition-colors duration-200 border-cyan-400 border-l-4 cursor-pointer min-w-50 sm:min-w-62.5 md:min-w-0"
|
||||
>
|
||||
<div
|
||||
style={contentStyle}
|
||||
className="lg:col-span-1 grid grid-cols-1 items-center justify-center py-2"
|
||||
>
|
||||
<div className="avatar">
|
||||
<div className="w-12 h-12 rounded-full border-2 flex items-center justify-center bg-base-300 border-cyan-400 border-l-4">
|
||||
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/${data.icon}`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
alt={text}
|
||||
width={125}
|
||||
height={125}
|
||||
className="w-8 h-8 object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NameAvatar
|
||||
locale={locale}
|
||||
text={getNameChar(locale, transI18n, data)}
|
||||
className="text-base-content text-center text-sm mt-1 font-medium"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 justify-center gap-2 py-2 w-full">
|
||||
<div className="bg-local text-primary text-xs max-w-full">
|
||||
{`${transI18n("useSkill")}: ${transI18n(attackTypeToString(turn.skillType).toLowerCase())}`}
|
||||
</div>
|
||||
<div className="text-primary text-xs max-w-full">
|
||||
{`${transI18n("totalDamage")}: ${Number(turn.totalDamage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}`}
|
||||
</div>
|
||||
<div className="text-primary text-xs max-w-full">
|
||||
{`${transI18n("actionValue")}: ${Number(turnHistory[turn.turnBattleId]?.actionValue || 0).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Character Detail Modal */}
|
||||
<dialog id="action_detail_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("action_detail_modal")}
|
||||
>
|
||||
✕
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
||||
{transI18n("turnDetail").toUpperCase()}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{selectAvatar && selectTurn && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="mt-4 w-full"
|
||||
>
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md mb-4">
|
||||
<h4 className="text-lg font-semibold mb-2 text-pink-500">{transI18n("characterInformation")}</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 items-center">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-base-content/70">{transI18n("id")}:</span>
|
||||
<span className="font-bold">{selectAvatar.id}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-base-content/70">{transI18n("character")}:</span>
|
||||
<NameAvatar
|
||||
locale={locale}
|
||||
text={getNameChar(locale, transI18n, selectAvatar)}
|
||||
className="font-bold"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center items-center">
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/${selectAvatar?.icon}`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
alt={getNameChar(locale, transI18n, selectAvatar)}
|
||||
width={80}
|
||||
height={80}
|
||||
className="h-20 w-20 object-cover rounded-full border-2 border-purple-500 shadow-lg shadow-purple-500/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("skillType")}</h4>
|
||||
<p className="mt-2">{transI18n(attackTypeToString(selectTurn?.skillType).toLowerCase())}</p>
|
||||
</div>
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("skillName")}</h4>
|
||||
<p className="mt-2">{selectTurn?.skillName}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("cycle")}</h4>
|
||||
<p className="mt-2">{turnHistory[selectTurn?.turnBattleId].cycleIndex}</p>
|
||||
</div>
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("wave")}</h4>
|
||||
<p className="mt-2">{turnHistory[selectTurn?.turnBattleId].waveIndex}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("actionValue")}</h4>
|
||||
<p className="mt-2">{Number(turnHistory[selectTurn?.turnBattleId].actionValue).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</p>
|
||||
</div>
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-purple-500 border-b border-purple-300/30 pb-1">{transI18n("totalDamage")}</h4>
|
||||
<p className="mt-2 font-bold text-lg">{Number(selectTurn?.totalDamage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md mb-4">
|
||||
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("damageDetails")}</h4>
|
||||
{selectTurn?.damageDetail?.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 mt-3">
|
||||
{selectTurn.damageDetail.map((detail, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex flex-col items-start gap-1 p-3 rounded-lg shadow bg-base-200"
|
||||
>
|
||||
<span className="text-lg font-semibold text-primary">
|
||||
{Number(detail.damage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}
|
||||
</span>
|
||||
<span className="text-xs uppercase text-gray-500 tracking-wide">
|
||||
{transI18n(attackTypeToString(detail?.damage_type).toLowerCase())}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-2 italic opacity-70">{transI18n("noDamageDetail")}</p>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
</dialog>
|
||||
</div>
|
||||
);
|
||||
"use client";
|
||||
import useAvatarDataStore from "@/stores/avatarDataStore";
|
||||
import useBattleDataStore from "@/stores/battleDataStore";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import { attackTypeToString, CharacterBasic } from "@/types";
|
||||
import { SkillBattleInfo } from "@/types/mics";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { motion } from "framer-motion";
|
||||
import { getNameChar } from "@/helper";
|
||||
import Image from "next/image";
|
||||
import NameAvatar from "../nameAvatar";
|
||||
|
||||
export default function ActionBar() {
|
||||
const [selectTurn, setSelectTurn] = useState<SkillBattleInfo | null>(null);
|
||||
const [selectAvatar, setSelectAvatar] = useState<CharacterBasic | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const { skillHistory, turnHistory, cycleIndex, waveIndex, maxWave } = useBattleDataStore();
|
||||
const { listAvatar } = useAvatarDataStore();
|
||||
const { locale } = useLocaleStore();
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const turnListRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
const contentStyle: React.CSSProperties = {
|
||||
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
const handleShow = (modalId: string, avatar: CharacterBasic, turn: SkillBattleInfo) => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
setSelectAvatar(avatar);
|
||||
setSelectTurn(turn);
|
||||
setIsModalOpen(true);
|
||||
modal.showModal();
|
||||
}
|
||||
};
|
||||
|
||||
// Close modal handler
|
||||
const handleCloseModal = (modalId: string) => {
|
||||
setIsModalOpen(false);
|
||||
setSelectAvatar(null);
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
modal.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle ESC key to close modal
|
||||
useEffect(() => {
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isModalOpen) {
|
||||
handleCloseModal("action_detail_modal");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleEscKey);
|
||||
return () => window.removeEventListener('keydown', handleEscKey);
|
||||
}, [isModalOpen]);
|
||||
|
||||
// Scroll to the bottom when new turns are added
|
||||
useEffect(() => {
|
||||
if (turnListRef.current && skillHistory.length > 0) {
|
||||
turnListRef.current.scrollTop = turnListRef.current.scrollHeight;
|
||||
}
|
||||
}, [skillHistory.length]);
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-1 rounded-lg shadow-lg w-full h-full">
|
||||
<motion.h2
|
||||
className="text-center text-xl lg:text-2xl mb-2 font-bold text-transparent bg-clip-text bg-linear-to-r from-pink-500 via-purple-500 to-cyan-500"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{transI18n("actionBar")}
|
||||
</motion.h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-1 xl:grid-cols-2 items-center justify-items-center w-full gap-2 mb-2 mx-2">
|
||||
<div className="badge badge-soft badge-info gap-2 text-lg py-3">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="size-6">
|
||||
<path fillRule="evenodd" d="M3 2.25a.75.75 0 0 1 .75.75v.54l1.838-.46a9.75 9.75 0 0 1 6.725.738l.108.054A8.25 8.25 0 0 0 18 4.524l3.11-.732a.75.75 0 0 1 .917.81 47.784 47.784 0 0 0 .005 10.337.75.75 0 0 1-.574.812l-3.114.733a9.75 9.75 0 0 1-6.594-.77l-.108-.054a8.25 8.25 0 0 0-5.69-.625l-2.202.55V21a.75.75 0 0 1-1.5 0V3A.75.75 0 0 1 3 2.25Z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<span>{waveIndex}/{maxWave}</span>
|
||||
</div>
|
||||
<div className="badge badge-soft badge-error gap-2 text-lg py-3 items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512" fill="currentColor" className="size-6">
|
||||
<path d="M0 32C0 14.3 14.3 0 32 0L64 0 320 0l32 0c17.7 0 32 14.3 32 32s-14.3 32-32 32l0 11c0 42.4-16.9 83.1-46.9 113.1L237.3 256l67.9 67.9c30 30 46.9 70.7 46.9 113.1l0 11c17.7 0 32 14.3 32 32s-14.3 32-32 32l-32 0L64 512l-32 0c-17.7 0-32-14.3-32-32s14.3-32 32-32l0-11c0-42.4 16.9-83.1 46.9-113.1L146.7 256 78.9 188.1C48.9 158.1 32 117.4 32 75l0-11C14.3 64 0 49.7 0 32zM96 64l0 11c0 25.5 10.1 49.9 28.1 67.9L192 210.7l67.9-67.9c18-18 28.1-42.4 28.1-67.9l0-11L96 64zm0 384l192 0 0-11c0-25.5-10.1-49.9-28.1-67.9L192 301.3l-67.9 67.9c-18 18-28.1 42.4-28.1 67.9l0 11z" />
|
||||
</svg>
|
||||
<span>{cycleIndex}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref={turnListRef}
|
||||
className="flex md:block px-2 md:px-0 w-full pt-2 border-t-2 border-accent overflow-x-auto md:overflow-x-hidden md:overflow-y-auto max-h-[90vh] lg:max-h-[80vh]"
|
||||
>
|
||||
<div className="flex flex-nowrap md:grid md:grid-cols-1 gap-2 w-fit md:w-full">
|
||||
{skillHistory.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<p className="text-base-content opacity-50">{transI18n("noTurns")}</p>
|
||||
</div>
|
||||
) : (
|
||||
skillHistory.map((turn, index) => {
|
||||
const data = listAvatar.find(it => it.id === turn.avatarId.toString());
|
||||
if (!data) return null;
|
||||
const text = getNameChar(locale, transI18n, data);
|
||||
|
||||
return (
|
||||
<div key={index} className="h-full md:w-full">
|
||||
<div
|
||||
onClick={() => handleShow("action_detail_modal", data, turn)}
|
||||
className="h-full grid grid-cols-2 gap-2 border bg-base-100 w-full hover:bg-base-200 transition-colors duration-200 border-cyan-400 border-l-4 cursor-pointer min-w-50 sm:min-w-62.5 md:min-w-0"
|
||||
>
|
||||
<div
|
||||
style={contentStyle}
|
||||
className="lg:col-span-1 grid grid-cols-1 items-center justify-center py-2"
|
||||
>
|
||||
<div className="avatar">
|
||||
<div className="w-12 h-12 rounded-full border-2 flex items-center justify-center bg-base-300 border-cyan-400 border-l-4">
|
||||
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/${data.icon}`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
alt={text}
|
||||
width={125}
|
||||
height={125}
|
||||
className="w-8 h-8 object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NameAvatar
|
||||
locale={locale}
|
||||
text={getNameChar(locale, transI18n, data)}
|
||||
className="text-base-content text-center text-sm mt-1 font-medium"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 justify-center gap-2 py-2 w-full">
|
||||
<div className="bg-local text-primary text-xs max-w-full">
|
||||
{`${transI18n("useSkill")}: ${transI18n(attackTypeToString(turn.skillType).toLowerCase())}`}
|
||||
</div>
|
||||
<div className="text-primary text-xs max-w-full">
|
||||
{`${transI18n("totalDamage")}: ${Number(turn.totalDamage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}`}
|
||||
</div>
|
||||
<div className="text-primary text-xs max-w-full">
|
||||
{`${transI18n("actionValue")}: ${Number(turnHistory[turn.turnBattleId]?.actionValue || 0).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Character Detail Modal */}
|
||||
<dialog id="action_detail_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("action_detail_modal")}
|
||||
>
|
||||
✕
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
||||
{transI18n("turnDetail").toUpperCase()}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{selectAvatar && selectTurn && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="mt-4 w-full"
|
||||
>
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md mb-4">
|
||||
<h4 className="text-lg font-semibold mb-2 text-pink-500">{transI18n("characterInformation")}</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 items-center">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-base-content/70">{transI18n("id")}:</span>
|
||||
<span className="font-bold">{selectAvatar.id}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-base-content/70">{transI18n("character")}:</span>
|
||||
<NameAvatar
|
||||
locale={locale}
|
||||
text={getNameChar(locale, transI18n, selectAvatar)}
|
||||
className="font-bold"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center items-center">
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/${selectAvatar?.icon}`}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
alt={getNameChar(locale, transI18n, selectAvatar)}
|
||||
width={80}
|
||||
height={80}
|
||||
className="h-20 w-20 object-cover rounded-full border-2 border-purple-500 shadow-lg shadow-purple-500/20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("skillType")}</h4>
|
||||
<p className="mt-2">{transI18n(attackTypeToString(selectTurn?.skillType).toLowerCase())}</p>
|
||||
</div>
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("skillName")}</h4>
|
||||
<p className="mt-2">{selectTurn?.skillName}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("cycle")}</h4>
|
||||
<p className="mt-2">{turnHistory[selectTurn?.turnBattleId].cycleIndex}</p>
|
||||
</div>
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("wave")}</h4>
|
||||
<p className="mt-2">{turnHistory[selectTurn?.turnBattleId].waveIndex}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("actionValue")}</h4>
|
||||
<p className="mt-2">{Number(turnHistory[selectTurn?.turnBattleId].actionValue).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</p>
|
||||
</div>
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-purple-500 border-b border-purple-300/30 pb-1">{transI18n("totalDamage")}</h4>
|
||||
<p className="mt-2 font-bold text-lg">{Number(selectTurn?.totalDamage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md mb-4">
|
||||
<h4 className="text-lg font-semibold mb-2 text-cyan-500 border-b border-cyan-300/30 pb-1">{transI18n("damageDetails")}</h4>
|
||||
{selectTurn?.damageDetail?.length > 0 ? (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 mt-3">
|
||||
{selectTurn.damageDetail.map((detail, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="flex flex-col items-start gap-1 p-3 rounded-lg shadow bg-base-200"
|
||||
>
|
||||
<span className="text-lg font-semibold text-primary">
|
||||
{Number(detail.damage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}
|
||||
</span>
|
||||
<span className="text-xs uppercase text-gray-500 tracking-wide">
|
||||
{transI18n(attackTypeToString(detail?.damage_type).toLowerCase())}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-2 italic opacity-70">{transI18n("noDamageDetail")}</p>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
</dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,99 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import { getNameChar } from '@/helper';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import { CharacterBasic } from '@/types';
|
||||
import NameAvatar from '../nameAvatar';
|
||||
import useBattleDataStore from '@/stores/battleDataStore';
|
||||
import Image from 'next/image';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface CharacterCardProps {
|
||||
data: CharacterBasic
|
||||
}
|
||||
|
||||
export default function CharacterCard({ data }: CharacterCardProps) {
|
||||
const { locale } = useLocaleStore();
|
||||
const { avatarDetail } = useBattleDataStore()
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const text = getNameChar(locale, transI18n, data)
|
||||
|
||||
return (
|
||||
<li className="z-10 flex flex-col w-28 items-center p-1 rounded-md shadow-lg bg-linear-to-b from-customStart to-customEnd transform transition-transform duration-300 hover:scale-105 m-1">
|
||||
<div
|
||||
className={`w-20 rounded-md p-0.5 bg-linear-to-br ${data.rank === "CombatPowerAvatarRarityType5"
|
||||
? "from-yellow-400 via-yellow-300 to-yellow-500"
|
||||
: "from-purple-300 via-purple-200 to-purple-400"
|
||||
}`}
|
||||
>
|
||||
|
||||
<div className="relative w-full h-full">
|
||||
<Image
|
||||
width={376}
|
||||
height={512}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${data.icon}`}
|
||||
className="w-full h-full rounded-md object-cover"
|
||||
alt="ALT"
|
||||
/>
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`/icon/${data?.damageType?.toLowerCase()}.webp`}
|
||||
className="absolute top-0 left-0 w-6 h-6"
|
||||
alt={data?.damageType?.toLowerCase()}
|
||||
/>
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`/icon/${data.baseType.toLowerCase()}.webp`}
|
||||
className="absolute top-0 right-0 w-6 h-6"
|
||||
alt={data.baseType.toLowerCase()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NameAvatar
|
||||
locale={locale}
|
||||
text={text}
|
||||
className="mt-2 text-center text-base font-normal leading-tight"
|
||||
/>
|
||||
{avatarDetail && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-base-content/70 mx-1">HP:</span>
|
||||
<span className="text-xs font-medium">
|
||||
<span className="text-error">
|
||||
{Number(avatarDetail?.[Number(data.id)]?.stats?.HP ?? 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}
|
||||
</span>
|
||||
<span className="text-base-content/50">/</span>
|
||||
<span className="text-base-content/70">
|
||||
{Number(avatarDetail?.[Number(data.id)]?.stats?.MaxHP ?? 100).toLocaleString(undefined, { maximumFractionDigits: 0 })}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full bg-base-300 rounded-full h-2.5">
|
||||
<div
|
||||
className="bg-error h-2.5 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.max(0, Math.min(100, ((avatarDetail?.[Number(data.id)]?.stats?.HP || 0) / (avatarDetail?.[Number(data.id)]?.stats?.MaxHP || 100)) * 100))}%`
|
||||
}}
|
||||
/>
|
||||
<span className="absolute inset-0 flex items-center justify-center text-xs text-white font-medium">
|
||||
{Math.round(((avatarDetail?.[Number(data.id)]?.stats?.HP || 0) / (avatarDetail?.[Number(data.id)]?.stats?.MaxHP || 100)) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
</li>
|
||||
|
||||
);
|
||||
}
|
||||
"use client";
|
||||
|
||||
import { getNameChar } from '@/helper';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import { CharacterBasic } from '@/types';
|
||||
import NameAvatar from '../nameAvatar';
|
||||
import useBattleDataStore from '@/stores/battleDataStore';
|
||||
import Image from 'next/image';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
interface CharacterCardProps {
|
||||
data: CharacterBasic
|
||||
}
|
||||
|
||||
export default function CharacterCard({ data }: CharacterCardProps) {
|
||||
const { locale } = useLocaleStore();
|
||||
const { avatarDetail } = useBattleDataStore()
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const text = getNameChar(locale, transI18n, data)
|
||||
|
||||
return (
|
||||
<li className="z-10 flex flex-col w-28 items-center p-1 rounded-md shadow-lg bg-linear-to-b from-customStart to-customEnd transform transition-transform duration-300 hover:scale-105 m-1">
|
||||
<div
|
||||
className={`w-20 rounded-md p-0.5 bg-linear-to-br ${data.rank === "CombatPowerAvatarRarityType5"
|
||||
? "from-yellow-400 via-yellow-300 to-yellow-500"
|
||||
: "from-purple-300 via-purple-200 to-purple-400"
|
||||
}`}
|
||||
>
|
||||
|
||||
<div className="relative w-full h-full">
|
||||
<Image
|
||||
width={376}
|
||||
height={512}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${data.icon}`}
|
||||
className="w-full h-full rounded-md object-cover"
|
||||
alt="ALT"
|
||||
/>
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`/icon/${data?.damageType?.toLowerCase()}.webp`}
|
||||
className="absolute top-0 left-0 w-6 h-6"
|
||||
alt={data?.damageType?.toLowerCase()}
|
||||
/>
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`/icon/${data.baseType.toLowerCase()}.webp`}
|
||||
className="absolute top-0 right-0 w-6 h-6"
|
||||
alt={data.baseType.toLowerCase()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NameAvatar
|
||||
locale={locale}
|
||||
text={text}
|
||||
className="mt-2 text-center text-base font-normal leading-tight"
|
||||
/>
|
||||
{avatarDetail && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-xs text-base-content/70 mx-1">HP:</span>
|
||||
<span className="text-xs font-medium">
|
||||
<span className="text-error">
|
||||
{Number(avatarDetail?.[Number(data.id)]?.stats?.HP ?? 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}
|
||||
</span>
|
||||
<span className="text-base-content/50">/</span>
|
||||
<span className="text-base-content/70">
|
||||
{Number(avatarDetail?.[Number(data.id)]?.stats?.MaxHP ?? 100).toLocaleString(undefined, { maximumFractionDigits: 0 })}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full bg-base-300 rounded-full h-2.5">
|
||||
<div
|
||||
className="bg-error h-2.5 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.max(0, Math.min(100, ((avatarDetail?.[Number(data.id)]?.stats?.HP || 0) / (avatarDetail?.[Number(data.id)]?.stats?.MaxHP || 100)) * 100))}%`
|
||||
}}
|
||||
/>
|
||||
<span className="absolute inset-0 flex items-center justify-center text-xs text-white font-medium">
|
||||
{Math.round(((avatarDetail?.[Number(data.id)]?.stats?.HP || 0) / (avatarDetail?.[Number(data.id)]?.stats?.MaxHP || 100)) * 100)}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)}
|
||||
|
||||
</li>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,84 +1,84 @@
|
||||
'use client';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
LineElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
Tooltip,
|
||||
Legend,
|
||||
} from 'chart.js';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
import { useDamageLinesForAll } from '@/hooks/useDamageLinesForAll';
|
||||
import useAvatarDataStore from '@/stores/avatarDataStore';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import { getNameChar } from '@/helper';
|
||||
import { useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
ChartJS.register(LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Legend);
|
||||
|
||||
const colors = ['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c'];
|
||||
|
||||
export default function MultiCharLineChart() {
|
||||
const [mode, setMode] = useState<1 | 2>(2);
|
||||
const dataByAvatar = useDamageLinesForAll(mode);
|
||||
const avatarIds = Object.keys(dataByAvatar).map(Number);
|
||||
const { mapAvatar } = useAvatarDataStore()
|
||||
const { locale } = useLocaleStore();
|
||||
const transI18n = useTranslations("DataAnalysisPage")
|
||||
|
||||
const data = {
|
||||
datasets: avatarIds.map((id, idx) => ({
|
||||
label: getNameChar(locale, transI18n, mapAvatar?.[id.toString()]),
|
||||
data: dataByAvatar[id].map(({ x, y }: { x: number; y: number }) => {
|
||||
return {
|
||||
x: x.toFixed(2),
|
||||
y: y.toFixed(2)
|
||||
}
|
||||
}),
|
||||
borderColor: colors[idx % colors.length],
|
||||
backgroundColor: colors[idx % colors.length],
|
||||
fill: false,
|
||||
tension: 0.3,
|
||||
})),
|
||||
};
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
type: 'linear' as const,
|
||||
position: 'bottom' as "bottom" | "center" | "left" | "top" | "right",
|
||||
ticks: {
|
||||
stepSize: 1,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||
{[
|
||||
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||
].map(({ mode: m, label, className }) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setMode(m as 1 | 2)}
|
||||
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Line data={data} options={options} />
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
'use client';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
LineElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
Tooltip,
|
||||
Legend,
|
||||
} from 'chart.js';
|
||||
import { Line } from 'react-chartjs-2';
|
||||
import { useDamageLinesForAll } from '@/hooks/useDamageLinesForAll';
|
||||
import useAvatarDataStore from '@/stores/avatarDataStore';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import { getNameChar } from '@/helper';
|
||||
import { useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
ChartJS.register(LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Legend);
|
||||
|
||||
const colors = ['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c'];
|
||||
|
||||
export default function MultiCharLineChart() {
|
||||
const [mode, setMode] = useState<1 | 2>(2);
|
||||
const dataByAvatar = useDamageLinesForAll(mode);
|
||||
const avatarIds = Object.keys(dataByAvatar).map(Number);
|
||||
const { mapAvatar } = useAvatarDataStore()
|
||||
const { locale } = useLocaleStore();
|
||||
const transI18n = useTranslations("DataAnalysisPage")
|
||||
|
||||
const data = {
|
||||
datasets: avatarIds.map((id, idx) => ({
|
||||
label: getNameChar(locale, transI18n, mapAvatar?.[id.toString()]),
|
||||
data: dataByAvatar[id].map(({ x, y }: { x: number; y: number }) => {
|
||||
return {
|
||||
x: x.toFixed(2),
|
||||
y: y.toFixed(2)
|
||||
}
|
||||
}),
|
||||
borderColor: colors[idx % colors.length],
|
||||
backgroundColor: colors[idx % colors.length],
|
||||
fill: false,
|
||||
tension: 0.3,
|
||||
})),
|
||||
};
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
scales: {
|
||||
x: {
|
||||
type: 'linear' as const,
|
||||
position: 'bottom' as "bottom" | "center" | "left" | "top" | "right",
|
||||
ticks: {
|
||||
stepSize: 1,
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||
{[
|
||||
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||
].map(({ mode: m, label, className }) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setMode(m as 1 | 2)}
|
||||
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Line data={data} options={options} />
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,80 +1,80 @@
|
||||
"use client";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import { ChartOptions } from "chart.js";
|
||||
import { useDamageLineForOne } from "@/hooks/useDamageLineForOne";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
LineElement,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
} from "chart.js";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
|
||||
ChartJS.register(
|
||||
LineElement,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
);
|
||||
|
||||
|
||||
export function DamageLineForOne({ avatarId }: { avatarId: number; }) {
|
||||
const [mode, setMode] = useState<0 | 1>(1);
|
||||
const dataRaw = useDamageLineForOne(avatarId, mode);
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const data = {
|
||||
labels: dataRaw.map(d => Number(d.x.toFixed(2))),
|
||||
datasets: [
|
||||
{
|
||||
label: mode === 1 ? transI18n("cumulativeDamage") : transI18n("damageByActionValue"),
|
||||
data: dataRaw.map(d => Number(d.y.toFixed(2))),
|
||||
borderColor: "rgba(75,192,192,1)",
|
||||
backgroundColor: "rgba(75,192,192,0.2)",
|
||||
tension: 0.3,
|
||||
fill: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options: ChartOptions<"line"> = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: true, text: transI18n("actionValue") } },
|
||||
y: { title: { display: true, text: transI18n("damage") }, beginAtZero: true },
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||
{[
|
||||
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||
].map(({ mode: m, label, className }) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setMode(m as 0 | 1)}
|
||||
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Line data={data} options={options} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
"use client";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import { ChartOptions } from "chart.js";
|
||||
import { useDamageLineForOne } from "@/hooks/useDamageLineForOne";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
LineElement,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler,
|
||||
} from "chart.js";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
|
||||
ChartJS.register(
|
||||
LineElement,
|
||||
PointElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
Filler
|
||||
);
|
||||
|
||||
|
||||
export function DamageLineForOne({ avatarId }: { avatarId: number; }) {
|
||||
const [mode, setMode] = useState<0 | 1>(1);
|
||||
const dataRaw = useDamageLineForOne(avatarId, mode);
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const data = {
|
||||
labels: dataRaw.map(d => Number(d.x.toFixed(2))),
|
||||
datasets: [
|
||||
{
|
||||
label: mode === 1 ? transI18n("cumulativeDamage") : transI18n("damageByActionValue"),
|
||||
data: dataRaw.map(d => Number(d.y.toFixed(2))),
|
||||
borderColor: "rgba(75,192,192,1)",
|
||||
backgroundColor: "rgba(75,192,192,0.2)",
|
||||
tension: 0.3,
|
||||
fill: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options: ChartOptions<"line"> = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: true, text: transI18n("actionValue") } },
|
||||
y: { title: { display: true, text: transI18n("damage") }, beginAtZero: true },
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||
{[
|
||||
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||
].map(({ mode: m, label, className }) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setMode(m as 0 | 1)}
|
||||
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Line data={data} options={options} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,177 +1,177 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { getNameChar } from '@/helper';
|
||||
import useAvatarDataStore from '@/stores/avatarDataStore';
|
||||
import useBattleDataStore from '@/stores/battleDataStore';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
Tooltip,
|
||||
Legend
|
||||
} from 'chart.js';
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import {attackTypeToString} from "@/types";
|
||||
|
||||
ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend);
|
||||
|
||||
const colorPalette = [
|
||||
'rgba(255, 99, 132, 0.6)',
|
||||
'rgba(54, 162, 235, 0.6)',
|
||||
'rgba(255, 206, 86, 0.6)',
|
||||
'rgba(75, 192, 192, 0.6)',
|
||||
'rgba(153, 102, 255, 0.6)',
|
||||
'rgba(255, 159, 64, 0.6)',
|
||||
'rgba(199, 199, 199, 0.6)',
|
||||
'rgba(255, 99, 255, 0.6)',
|
||||
'rgba(0, 200, 83, 0.6)',
|
||||
'rgba(255, 112, 67, 0.6)',
|
||||
'rgba(63, 81, 181, 0.6)',
|
||||
'rgba(0, 188, 212, 0.6)',
|
||||
'rgba(233, 30, 99, 0.6)',
|
||||
'rgba(124, 179, 66, 0.6)',
|
||||
'rgba(255, 235, 59, 0.6)',
|
||||
'rgba(158, 158, 158, 0.6)',
|
||||
'rgba(121, 85, 72, 0.6)',
|
||||
'rgba(96, 125, 139, 0.6)',
|
||||
'rgba(100, 181, 246, 0.6)',
|
||||
'rgba(255, 171, 145, 0.6)',
|
||||
'rgba(174, 213, 129, 0.6)',
|
||||
'rgba(255, 245, 157, 0.6)'
|
||||
];
|
||||
|
||||
const borderPalette = colorPalette.map((c) => c.replace('0.6', '1'));
|
||||
|
||||
export default function DamagePerAvatarForAll() {
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const { lineup, skillHistory } = useBattleDataStore();
|
||||
const { mapAvatar } = useAvatarDataStore();
|
||||
const { locale } = useLocaleStore();
|
||||
|
||||
const [mode, setMode] = useState<number>(2);
|
||||
|
||||
const avatarMap = lineup.map((avatar) => {
|
||||
if (!mapAvatar?.[avatar.avatarId.toString()]) return undefined;
|
||||
return {
|
||||
avatarId: avatar.avatarId,
|
||||
avatarName: getNameChar(locale, transI18n, mapAvatar?.[avatar.avatarId.toString()])
|
||||
};
|
||||
}).filter(Boolean) as { avatarId: number, avatarName: string }[];
|
||||
|
||||
const labels = avatarMap.map(a => a.avatarName);
|
||||
|
||||
let datasets: Array<{
|
||||
label: string;
|
||||
data: number[];
|
||||
backgroundColor: string | string[];
|
||||
borderColor: string | string[];
|
||||
borderWidth: number;
|
||||
stack?: string;
|
||||
}> = [];
|
||||
|
||||
if (mode === 1) {
|
||||
datasets = [
|
||||
{
|
||||
label: transI18n("totalDamage"),
|
||||
data: avatarMap.map(({ avatarId }) => {
|
||||
const total = skillHistory
|
||||
.filter(skill => skill.avatarId === avatarId)
|
||||
.reduce((sum, s) => sum + s.totalDamage, 0);
|
||||
return total;
|
||||
}),
|
||||
backgroundColor: avatarMap.map((_, i) => colorPalette[i % colorPalette.length]),
|
||||
borderColor: avatarMap.map((_, i) => borderPalette[i % borderPalette.length]),
|
||||
borderWidth: 1
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (mode === 2) {
|
||||
const damageTypesSet = new Set<string>();
|
||||
avatarMap.forEach(({ avatarId }) => {
|
||||
skillHistory.filter(s => s.avatarId === avatarId).forEach(s =>
|
||||
s.damageDetail?.forEach(d => d.damage_type && damageTypesSet.add(transI18n(attackTypeToString(d?.damage_type).toLowerCase())))
|
||||
);
|
||||
});
|
||||
const damageTypes = Array.from(damageTypesSet);
|
||||
|
||||
datasets = damageTypes.map((dt, i) => ({
|
||||
label: dt,
|
||||
data: avatarMap.map(({ avatarId }) => {
|
||||
const total = skillHistory
|
||||
.filter(skill => skill.avatarId === avatarId)
|
||||
.flatMap(s => s.damageDetail || [])
|
||||
.filter(d => transI18n(attackTypeToString(d?.damage_type).toLowerCase()) === dt)
|
||||
.reduce((sum, d) => sum + d.damage, 0);
|
||||
return total;
|
||||
}),
|
||||
backgroundColor: colorPalette[i % colorPalette.length],
|
||||
borderColor: borderPalette[i % borderPalette.length],
|
||||
borderWidth: 1,
|
||||
stack: transI18n('damage')
|
||||
}));
|
||||
}
|
||||
|
||||
if (mode === 3) {
|
||||
const skillTypesSet = new Set<string>();
|
||||
skillHistory.forEach(s => skillTypesSet.add(transI18n(attackTypeToString(s?.skillType)?.toLowerCase())));
|
||||
const skillTypes = Array.from(skillTypesSet);
|
||||
|
||||
datasets = skillTypes.map((st, i) => ({
|
||||
label: st,
|
||||
data: avatarMap.map(({ avatarId }) => {
|
||||
const total = skillHistory
|
||||
.filter(skill => skill.avatarId === avatarId && transI18n(attackTypeToString(skill?.skillType).toLowerCase()) === st)
|
||||
.reduce((sum, s) => sum + s.totalDamage, 0);
|
||||
return total;
|
||||
}),
|
||||
backgroundColor: colorPalette[i % colorPalette.length],
|
||||
borderColor: borderPalette[i % borderPalette.length],
|
||||
borderWidth: 1,
|
||||
stack: transI18n('skill')
|
||||
}));
|
||||
}
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
stacked: mode !== 1,
|
||||
ticks: {
|
||||
precision: 0,
|
||||
},
|
||||
},
|
||||
x: {
|
||||
stacked: mode !== 1,
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||
{[
|
||||
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||
{ mode: 3, label: `${transI18n("type")} 3`, className: "btn-accent" },
|
||||
].map(({ mode: m, label, className }) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setMode(m)}
|
||||
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Bar data={{ labels, datasets }} options={options}/>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { getNameChar } from '@/helper';
|
||||
import useAvatarDataStore from '@/stores/avatarDataStore';
|
||||
import useBattleDataStore from '@/stores/battleDataStore';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
BarElement,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
Tooltip,
|
||||
Legend
|
||||
} from 'chart.js';
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import {attackTypeToString} from "@/types";
|
||||
|
||||
ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend);
|
||||
|
||||
const colorPalette = [
|
||||
'rgba(255, 99, 132, 0.6)',
|
||||
'rgba(54, 162, 235, 0.6)',
|
||||
'rgba(255, 206, 86, 0.6)',
|
||||
'rgba(75, 192, 192, 0.6)',
|
||||
'rgba(153, 102, 255, 0.6)',
|
||||
'rgba(255, 159, 64, 0.6)',
|
||||
'rgba(199, 199, 199, 0.6)',
|
||||
'rgba(255, 99, 255, 0.6)',
|
||||
'rgba(0, 200, 83, 0.6)',
|
||||
'rgba(255, 112, 67, 0.6)',
|
||||
'rgba(63, 81, 181, 0.6)',
|
||||
'rgba(0, 188, 212, 0.6)',
|
||||
'rgba(233, 30, 99, 0.6)',
|
||||
'rgba(124, 179, 66, 0.6)',
|
||||
'rgba(255, 235, 59, 0.6)',
|
||||
'rgba(158, 158, 158, 0.6)',
|
||||
'rgba(121, 85, 72, 0.6)',
|
||||
'rgba(96, 125, 139, 0.6)',
|
||||
'rgba(100, 181, 246, 0.6)',
|
||||
'rgba(255, 171, 145, 0.6)',
|
||||
'rgba(174, 213, 129, 0.6)',
|
||||
'rgba(255, 245, 157, 0.6)'
|
||||
];
|
||||
|
||||
const borderPalette = colorPalette.map((c) => c.replace('0.6', '1'));
|
||||
|
||||
export default function DamagePerAvatarForAll() {
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const { lineup, skillHistory } = useBattleDataStore();
|
||||
const { mapAvatar } = useAvatarDataStore();
|
||||
const { locale } = useLocaleStore();
|
||||
|
||||
const [mode, setMode] = useState<number>(2);
|
||||
|
||||
const avatarMap = lineup.map((avatar) => {
|
||||
if (!mapAvatar?.[avatar.avatarId.toString()]) return undefined;
|
||||
return {
|
||||
avatarId: avatar.avatarId,
|
||||
avatarName: getNameChar(locale, transI18n, mapAvatar?.[avatar.avatarId.toString()])
|
||||
};
|
||||
}).filter(Boolean) as { avatarId: number, avatarName: string }[];
|
||||
|
||||
const labels = avatarMap.map(a => a.avatarName);
|
||||
|
||||
let datasets: Array<{
|
||||
label: string;
|
||||
data: number[];
|
||||
backgroundColor: string | string[];
|
||||
borderColor: string | string[];
|
||||
borderWidth: number;
|
||||
stack?: string;
|
||||
}> = [];
|
||||
|
||||
if (mode === 1) {
|
||||
datasets = [
|
||||
{
|
||||
label: transI18n("totalDamage"),
|
||||
data: avatarMap.map(({ avatarId }) => {
|
||||
const total = skillHistory
|
||||
.filter(skill => skill.avatarId === avatarId)
|
||||
.reduce((sum, s) => sum + s.totalDamage, 0);
|
||||
return total;
|
||||
}),
|
||||
backgroundColor: avatarMap.map((_, i) => colorPalette[i % colorPalette.length]),
|
||||
borderColor: avatarMap.map((_, i) => borderPalette[i % borderPalette.length]),
|
||||
borderWidth: 1
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
if (mode === 2) {
|
||||
const damageTypesSet = new Set<string>();
|
||||
avatarMap.forEach(({ avatarId }) => {
|
||||
skillHistory.filter(s => s.avatarId === avatarId).forEach(s =>
|
||||
s.damageDetail?.forEach(d => d.damage_type && damageTypesSet.add(transI18n(attackTypeToString(d?.damage_type).toLowerCase())))
|
||||
);
|
||||
});
|
||||
const damageTypes = Array.from(damageTypesSet);
|
||||
|
||||
datasets = damageTypes.map((dt, i) => ({
|
||||
label: dt,
|
||||
data: avatarMap.map(({ avatarId }) => {
|
||||
const total = skillHistory
|
||||
.filter(skill => skill.avatarId === avatarId)
|
||||
.flatMap(s => s.damageDetail || [])
|
||||
.filter(d => transI18n(attackTypeToString(d?.damage_type).toLowerCase()) === dt)
|
||||
.reduce((sum, d) => sum + d.damage, 0);
|
||||
return total;
|
||||
}),
|
||||
backgroundColor: colorPalette[i % colorPalette.length],
|
||||
borderColor: borderPalette[i % borderPalette.length],
|
||||
borderWidth: 1,
|
||||
stack: transI18n('damage')
|
||||
}));
|
||||
}
|
||||
|
||||
if (mode === 3) {
|
||||
const skillTypesSet = new Set<string>();
|
||||
skillHistory.forEach(s => skillTypesSet.add(transI18n(attackTypeToString(s?.skillType)?.toLowerCase())));
|
||||
const skillTypes = Array.from(skillTypesSet);
|
||||
|
||||
datasets = skillTypes.map((st, i) => ({
|
||||
label: st,
|
||||
data: avatarMap.map(({ avatarId }) => {
|
||||
const total = skillHistory
|
||||
.filter(skill => skill.avatarId === avatarId && transI18n(attackTypeToString(skill?.skillType).toLowerCase()) === st)
|
||||
.reduce((sum, s) => sum + s.totalDamage, 0);
|
||||
return total;
|
||||
}),
|
||||
backgroundColor: colorPalette[i % colorPalette.length],
|
||||
borderColor: borderPalette[i % borderPalette.length],
|
||||
borderWidth: 1,
|
||||
stack: transI18n('skill')
|
||||
}));
|
||||
}
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
stacked: mode !== 1,
|
||||
ticks: {
|
||||
precision: 0,
|
||||
},
|
||||
},
|
||||
x: {
|
||||
stacked: mode !== 1,
|
||||
},
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||
{[
|
||||
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||
{ mode: 3, label: `${transI18n("type")} 3`, className: "btn-accent" },
|
||||
].map(({ mode: m, label, className }) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setMode(m)}
|
||||
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Bar data={{ labels, datasets }} options={options}/>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
"use client";
|
||||
import { Bar } from "react-chartjs-2";
|
||||
import { ChartOptions } from "chart.js";
|
||||
import { useDamagePerCycleForAll } from "@/hooks/useDamagePerCycle";
|
||||
import { Chart as ChartJS, BarElement, ArcElement, RadialLinearScale } from "chart.js";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
|
||||
ChartJS.register(BarElement, ArcElement, RadialLinearScale);
|
||||
export default function DamagePerCycleForAll() {
|
||||
const [mode, setMode] = useState<0 | 1 | 2>(0);
|
||||
const dataRaw = useDamagePerCycleForAll(mode);
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const data = {
|
||||
labels: dataRaw.map(d => d.x),
|
||||
datasets: [
|
||||
{
|
||||
label: mode === 0 ? `${transI18n("damagePerCycleAndWave")}` : mode === 1 ? `${transI18n("damagePerCycle")}` : `${transI18n("damagePerWave")}`,
|
||||
data: dataRaw.map(d => d.y),
|
||||
backgroundColor: dataRaw.map((_, i) =>
|
||||
['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c', '#f472b6'][i % 7]
|
||||
),
|
||||
},
|
||||
],
|
||||
|
||||
};
|
||||
|
||||
const options: ChartOptions<"bar"> = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: true, text: transI18n("cycleCount") } },
|
||||
y: { title: { display: true, text: transI18n("totalDamage") }, beginAtZero: true },
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||
{[
|
||||
{ mode: 0, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||
{ mode: 1, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||
{ mode: 2, label: `${transI18n("type")} 3`, className: "btn-accent" },
|
||||
].map(({ mode: m, label, className }) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setMode(m as 0 | 1 | 2)}
|
||||
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Bar data={data} options={options} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
"use client";
|
||||
import { Bar } from "react-chartjs-2";
|
||||
import { ChartOptions } from "chart.js";
|
||||
import { useDamagePerCycleForAll } from "@/hooks/useDamagePerCycle";
|
||||
import { Chart as ChartJS, BarElement, ArcElement, RadialLinearScale } from "chart.js";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
|
||||
ChartJS.register(BarElement, ArcElement, RadialLinearScale);
|
||||
export default function DamagePerCycleForAll() {
|
||||
const [mode, setMode] = useState<0 | 1 | 2>(0);
|
||||
const dataRaw = useDamagePerCycleForAll(mode);
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const data = {
|
||||
labels: dataRaw.map(d => d.x),
|
||||
datasets: [
|
||||
{
|
||||
label: mode === 0 ? `${transI18n("damagePerCycleAndWave")}` : mode === 1 ? `${transI18n("damagePerCycle")}` : `${transI18n("damagePerWave")}`,
|
||||
data: dataRaw.map(d => d.y),
|
||||
backgroundColor: dataRaw.map((_, i) =>
|
||||
['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c', '#f472b6'][i % 7]
|
||||
),
|
||||
},
|
||||
],
|
||||
|
||||
};
|
||||
|
||||
const options: ChartOptions<"bar"> = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: true, text: transI18n("cycleCount") } },
|
||||
y: { title: { display: true, text: transI18n("totalDamage") }, beginAtZero: true },
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||
{[
|
||||
{ mode: 0, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||
{ mode: 1, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||
{ mode: 2, label: `${transI18n("type")} 3`, className: "btn-accent" },
|
||||
].map(({ mode: m, label, className }) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setMode(m as 0 | 1 | 2)}
|
||||
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Bar data={data} options={options} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,64 +1,64 @@
|
||||
"use client";
|
||||
import { Bar } from "react-chartjs-2";
|
||||
import { ChartOptions } from "chart.js";
|
||||
import { useDamagePerCycleForOne } from "@/hooks/useDamagePerCycle";
|
||||
|
||||
import { Chart as ChartJS, BarElement, ArcElement, RadialLinearScale } from "chart.js";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
|
||||
ChartJS.register(BarElement, ArcElement, RadialLinearScale);
|
||||
export function DamagePerCycleForOne({
|
||||
avatarId,
|
||||
}: {
|
||||
avatarId: number;
|
||||
}) {
|
||||
const [mode, setMode] = useState<0 | 1 | 2>(0);
|
||||
const dataRaw = useDamagePerCycleForOne(avatarId, mode);
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const data = {
|
||||
labels: dataRaw.map(d => d.x),
|
||||
datasets: [
|
||||
{
|
||||
label: mode === 0 ? `${transI18n("damagePerCycleAndWave")}` : mode === 1 ? `${transI18n("damagePerCycle")}` : `${transI18n("damagePerWave")}`,
|
||||
data: dataRaw.map(d => d.y),
|
||||
backgroundColor: dataRaw.map((_, i) =>
|
||||
['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c', '#f472b6'][i % 7]
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options: ChartOptions<"bar"> = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: true, text: transI18n("cycleCount") } },
|
||||
y: { title: { display: true, text: transI18n("totalDamage") }, beginAtZero: true },
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||
{[
|
||||
{ mode: 0, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||
{ mode: 1, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||
{ mode: 2, label: `${transI18n("type")} 3`, className: "btn-warning" },
|
||||
].map(({ mode: m, label, className }) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setMode(m as 0 | 1 | 2)}
|
||||
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Bar data={data} options={options} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
"use client";
|
||||
import { Bar } from "react-chartjs-2";
|
||||
import { ChartOptions } from "chart.js";
|
||||
import { useDamagePerCycleForOne } from "@/hooks/useDamagePerCycle";
|
||||
|
||||
import { Chart as ChartJS, BarElement, ArcElement, RadialLinearScale } from "chart.js";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState } from "react";
|
||||
|
||||
ChartJS.register(BarElement, ArcElement, RadialLinearScale);
|
||||
export function DamagePerCycleForOne({
|
||||
avatarId,
|
||||
}: {
|
||||
avatarId: number;
|
||||
}) {
|
||||
const [mode, setMode] = useState<0 | 1 | 2>(0);
|
||||
const dataRaw = useDamagePerCycleForOne(avatarId, mode);
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const data = {
|
||||
labels: dataRaw.map(d => d.x),
|
||||
datasets: [
|
||||
{
|
||||
label: mode === 0 ? `${transI18n("damagePerCycleAndWave")}` : mode === 1 ? `${transI18n("damagePerCycle")}` : `${transI18n("damagePerWave")}`,
|
||||
data: dataRaw.map(d => d.y),
|
||||
backgroundColor: dataRaw.map((_, i) =>
|
||||
['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c', '#f472b6'][i % 7]
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options: ChartOptions<"bar"> = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { display: true },
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: true, text: transI18n("cycleCount") } },
|
||||
y: { title: { display: true, text: transI18n("totalDamage") }, beginAtZero: true },
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||
{[
|
||||
{ mode: 0, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||
{ mode: 1, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||
{ mode: 2, label: `${transI18n("type")} 3`, className: "btn-warning" },
|
||||
].map(({ mode: m, label, className }) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setMode(m as 0 | 1 | 2)}
|
||||
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Bar data={data} options={options} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,91 +1,91 @@
|
||||
'use client';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
ArcElement,
|
||||
Tooltip,
|
||||
Legend
|
||||
} from 'chart.js';
|
||||
import { Pie } from 'react-chartjs-2';
|
||||
import { useDamagePercentPerAvatar } from '@/hooks/useDamagePercentPerAvatar';
|
||||
import useAvatarDataStore from '@/stores/avatarDataStore';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import { getNameChar } from '@/helper';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { useDamagePercentByType } from '@/hooks/useDamagePercentForDamgeType';
|
||||
|
||||
ChartJS.register(ArcElement, Tooltip, Legend);
|
||||
|
||||
const colors = [
|
||||
'#f87171', '#34d399', '#60a5fa', '#facc15',
|
||||
'#a78bfa', '#fb923c', '#f472b6', '#10b981',
|
||||
'#fcd34d', '#c084fc', '#f97316', '#ec4899',
|
||||
'#4ade80', '#fbbf24', '#8b5cf6', '#f43f5e'
|
||||
];
|
||||
|
||||
export default function DamagePercentChartForAll() {
|
||||
const [mode, setMode] = useState<1 | 2>(1);
|
||||
const damageByAvatar = useDamagePercentPerAvatar();
|
||||
const damageByType = useDamagePercentByType();
|
||||
const { mapAvatar } = useAvatarDataStore();
|
||||
const { locale } = useLocaleStore();
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
|
||||
const chartData = {
|
||||
labels: (mode === 1
|
||||
? damageByAvatar.map(d =>
|
||||
getNameChar(locale, transI18n, mapAvatar?.[d.avatarId.toString()])
|
||||
)
|
||||
: damageByType.map(d => transI18n(d.type.toLowerCase()))
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: '% ' + transI18n("damage"),
|
||||
data: (mode === 1
|
||||
? damageByAvatar.map(d => d.percent.toFixed(2))
|
||||
: damageByType.map(d => d.percent.toFixed(2))
|
||||
),
|
||||
backgroundColor: colors,
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (ctx: import('chart.js').TooltipItem<'pie'>) => `${ctx.label}: ${ctx.raw}%`,
|
||||
},
|
||||
},
|
||||
datalabels: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||
{[
|
||||
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||
].map(({ mode: m, label, className }) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setMode(m as 1 | 2)}
|
||||
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Pie data={chartData} options={options} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
'use client';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
ArcElement,
|
||||
Tooltip,
|
||||
Legend
|
||||
} from 'chart.js';
|
||||
import { Pie } from 'react-chartjs-2';
|
||||
import { useDamagePercentPerAvatar } from '@/hooks/useDamagePercentPerAvatar';
|
||||
import useAvatarDataStore from '@/stores/avatarDataStore';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import { getNameChar } from '@/helper';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { useDamagePercentByType } from '@/hooks/useDamagePercentForDamgeType';
|
||||
|
||||
ChartJS.register(ArcElement, Tooltip, Legend);
|
||||
|
||||
const colors = [
|
||||
'#f87171', '#34d399', '#60a5fa', '#facc15',
|
||||
'#a78bfa', '#fb923c', '#f472b6', '#10b981',
|
||||
'#fcd34d', '#c084fc', '#f97316', '#ec4899',
|
||||
'#4ade80', '#fbbf24', '#8b5cf6', '#f43f5e'
|
||||
];
|
||||
|
||||
export default function DamagePercentChartForAll() {
|
||||
const [mode, setMode] = useState<1 | 2>(1);
|
||||
const damageByAvatar = useDamagePercentPerAvatar();
|
||||
const damageByType = useDamagePercentByType();
|
||||
const { mapAvatar } = useAvatarDataStore();
|
||||
const { locale } = useLocaleStore();
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
|
||||
const chartData = {
|
||||
labels: (mode === 1
|
||||
? damageByAvatar.map(d =>
|
||||
getNameChar(locale, transI18n, mapAvatar?.[d.avatarId.toString()])
|
||||
)
|
||||
: damageByType.map(d => transI18n(d.type.toLowerCase()))
|
||||
),
|
||||
datasets: [
|
||||
{
|
||||
label: '% ' + transI18n("damage"),
|
||||
data: (mode === 1
|
||||
? damageByAvatar.map(d => d.percent.toFixed(2))
|
||||
: damageByType.map(d => d.percent.toFixed(2))
|
||||
),
|
||||
backgroundColor: colors,
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: (ctx: import('chart.js').TooltipItem<'pie'>) => `${ctx.label}: ${ctx.raw}%`,
|
||||
},
|
||||
},
|
||||
datalabels: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='w-full'>
|
||||
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||
{[
|
||||
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||
].map(({ mode: m, label, className }) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setMode(m as 1 | 2)}
|
||||
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<Pie data={chartData} options={options} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,70 +1,70 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
import { Chart as ChartJS, BarElement, CategoryScale, LinearScale, Tooltip, Legend } from 'chart.js';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSkillDamageForAvatar } from '@/hooks/useSkillDamageForAvatar';
|
||||
import { useDamageByTypeForAvatar } from '@/hooks/useDamageTypeForAvatar';
|
||||
|
||||
|
||||
ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend);
|
||||
|
||||
export default function SkillBarChart({ avatarId }: { avatarId: number }) {
|
||||
const [mode, setMode] = useState<1 | 2>(2);
|
||||
|
||||
const transI18n = useTranslations('DataAnalysisPage');
|
||||
const { labels: skillLabels, damageValues: skillDamageValues } = useSkillDamageForAvatar(avatarId);
|
||||
const { labels: typeLabels, damageValues: typeDamageValues } = useDamageByTypeForAvatar(avatarId);
|
||||
|
||||
const colors = ['#f87171', '#facc15', '#34d399', '#60a5fa', '#a78bfa', '#fb923c'];
|
||||
|
||||
const data = {
|
||||
labels: mode === 1 ? skillLabels.map(it => transI18n(it)) : typeLabels.map(it => transI18n(it)),
|
||||
datasets: [
|
||||
{
|
||||
label: mode === 1
|
||||
? transI18n('skill')
|
||||
: transI18n('damage'),
|
||||
data: mode === 1 ? skillDamageValues : typeDamageValues,
|
||||
backgroundColor: colors.slice(0, (mode === 1 ? skillLabels : typeLabels).length),
|
||||
borderColor: '#111',
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: { precision: 0 },
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||
{[
|
||||
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||
].map(({ mode: m, label, className }) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setMode(m as 1 | 2)}
|
||||
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Bar data={data} options={options} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
import { Chart as ChartJS, BarElement, CategoryScale, LinearScale, Tooltip, Legend } from 'chart.js';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSkillDamageForAvatar } from '@/hooks/useSkillDamageForAvatar';
|
||||
import { useDamageByTypeForAvatar } from '@/hooks/useDamageTypeForAvatar';
|
||||
|
||||
|
||||
ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend);
|
||||
|
||||
export default function SkillBarChart({ avatarId }: { avatarId: number }) {
|
||||
const [mode, setMode] = useState<1 | 2>(2);
|
||||
|
||||
const transI18n = useTranslations('DataAnalysisPage');
|
||||
const { labels: skillLabels, damageValues: skillDamageValues } = useSkillDamageForAvatar(avatarId);
|
||||
const { labels: typeLabels, damageValues: typeDamageValues } = useDamageByTypeForAvatar(avatarId);
|
||||
|
||||
const colors = ['#f87171', '#facc15', '#34d399', '#60a5fa', '#a78bfa', '#fb923c'];
|
||||
|
||||
const data = {
|
||||
labels: mode === 1 ? skillLabels.map(it => transI18n(it)) : typeLabels.map(it => transI18n(it)),
|
||||
datasets: [
|
||||
{
|
||||
label: mode === 1
|
||||
? transI18n('skill')
|
||||
: transI18n('damage'),
|
||||
data: mode === 1 ? skillDamageValues : typeDamageValues,
|
||||
backgroundColor: colors.slice(0, (mode === 1 ? skillLabels : typeLabels).length),
|
||||
borderColor: '#111',
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: { precision: 0 },
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||
{[
|
||||
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||
].map(({ mode: m, label, className }) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setMode(m as 1 | 2)}
|
||||
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Bar data={data} options={options} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,75 +1,75 @@
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { Pie } from 'react-chartjs-2';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
ArcElement,
|
||||
Tooltip,
|
||||
Legend,
|
||||
} from 'chart.js';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSkillDamageForAvatar } from '@/hooks/useSkillDamageForAvatar';
|
||||
import { useDamageByTypeForAvatar } from '@/hooks/useDamageTypeForAvatar';
|
||||
|
||||
ChartJS.register(ArcElement, Tooltip, Legend);
|
||||
|
||||
const COLORS = [
|
||||
'#f87171', '#facc15', '#34d399', '#60a5fa', '#a78bfa', '#fb923c',
|
||||
'#f472b6', '#38bdf8', '#a3e635', '#fdba74', '#c084fc', '#4ade80',
|
||||
'#fcd34d', '#818cf8', '#f43f5e', '#14b8a6', '#e879f9', '#86efac',
|
||||
];
|
||||
|
||||
export default function SkillPieChart({ avatarId }: { avatarId: number }) {
|
||||
const [mode, setMode] = useState<1 | 2>(2);
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
|
||||
const skillData = useSkillDamageForAvatar(avatarId);
|
||||
const typeData = useDamageByTypeForAvatar(avatarId);
|
||||
|
||||
const { labels, damageValues } = mode === 1 ? skillData : typeData;
|
||||
const total = damageValues.reduce((sum, val) => sum + val, 0);
|
||||
|
||||
const labelsWithPercent = labels.map((label, index) => {
|
||||
const value = damageValues[index];
|
||||
const percent = total ? ((value / total) * 100).toFixed(1) : '0.0';
|
||||
return `${transI18n(label.toLowerCase())} (${percent}%)`;
|
||||
});
|
||||
|
||||
const data = {
|
||||
labels: labelsWithPercent,
|
||||
datasets: [
|
||||
{
|
||||
data: damageValues,
|
||||
backgroundColor: COLORS.slice(0, labels.length),
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full ">
|
||||
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||
{[
|
||||
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||
].map(({ mode: m, label, className }) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setMode(m as 1 | 2)}
|
||||
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Pie
|
||||
data={data}
|
||||
options={{
|
||||
responsive: true,
|
||||
cutout: '30%',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { Pie } from 'react-chartjs-2';
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
ArcElement,
|
||||
Tooltip,
|
||||
Legend,
|
||||
} from 'chart.js';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSkillDamageForAvatar } from '@/hooks/useSkillDamageForAvatar';
|
||||
import { useDamageByTypeForAvatar } from '@/hooks/useDamageTypeForAvatar';
|
||||
|
||||
ChartJS.register(ArcElement, Tooltip, Legend);
|
||||
|
||||
const COLORS = [
|
||||
'#f87171', '#facc15', '#34d399', '#60a5fa', '#a78bfa', '#fb923c',
|
||||
'#f472b6', '#38bdf8', '#a3e635', '#fdba74', '#c084fc', '#4ade80',
|
||||
'#fcd34d', '#818cf8', '#f43f5e', '#14b8a6', '#e879f9', '#86efac',
|
||||
];
|
||||
|
||||
export default function SkillPieChart({ avatarId }: { avatarId: number }) {
|
||||
const [mode, setMode] = useState<1 | 2>(2);
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
|
||||
const skillData = useSkillDamageForAvatar(avatarId);
|
||||
const typeData = useDamageByTypeForAvatar(avatarId);
|
||||
|
||||
const { labels, damageValues } = mode === 1 ? skillData : typeData;
|
||||
const total = damageValues.reduce((sum, val) => sum + val, 0);
|
||||
|
||||
const labelsWithPercent = labels.map((label, index) => {
|
||||
const value = damageValues[index];
|
||||
const percent = total ? ((value / total) * 100).toFixed(1) : '0.0';
|
||||
return `${transI18n(label.toLowerCase())} (${percent}%)`;
|
||||
});
|
||||
|
||||
const data = {
|
||||
labels: labelsWithPercent,
|
||||
datasets: [
|
||||
{
|
||||
data: damageValues,
|
||||
backgroundColor: COLORS.slice(0, labels.length),
|
||||
borderColor: '#fff',
|
||||
borderWidth: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full ">
|
||||
<div className="mb-4 flex items-start gap-2 justify-end">
|
||||
{[
|
||||
{ mode: 1, label: `${transI18n("type")} 1`, className: "btn-primary" },
|
||||
{ mode: 2, label: `${transI18n("type")} 2`, className: "btn-warning" },
|
||||
].map(({ mode: m, label, className }) => (
|
||||
<button
|
||||
key={m}
|
||||
onClick={() => setMode(m as 1 | 2)}
|
||||
className={`btn btn-sm ${mode === m ? className : "btn-ghost"}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<Pie
|
||||
data={data}
|
||||
options={{
|
||||
responsive: true,
|
||||
cutout: '30%',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,79 +1,79 @@
|
||||
"use client"
|
||||
|
||||
import useBattleDataStore from "@/stores/battleDataStore";
|
||||
import Image from "next/image";
|
||||
import useAvatarDataStore from "@/stores/avatarDataStore";
|
||||
import { getNameEnemy } from "@/helper/getNameChar";
|
||||
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import NameAvatar from "../nameAvatar";
|
||||
|
||||
export default function EnemyBar() {
|
||||
const { enemyDetail } = useBattleDataStore()
|
||||
const { mapEnemy } = useAvatarDataStore()
|
||||
const { locale } = useLocaleStore()
|
||||
|
||||
return (
|
||||
<div className="p-3 w-full">
|
||||
<div className="flex gap-3 overflow-x-auto pb-2">
|
||||
{enemyDetail && Object.values(enemyDetail).filter((enemy) => (
|
||||
enemy.stats.HP <= enemy.maxHP)).map((enemy, uid) => (
|
||||
<div key={uid} className="bg-base-200 rounded-lg p-3 border border-gray-700 w-52 shrink-0">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{mapEnemy?.[enemy.id.toString()]?.icon && (
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/${mapEnemy?.[enemy.id.toString()]?.icon}`}
|
||||
alt={enemy.name}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
width={40}
|
||||
height={40}
|
||||
className="object-cover w-10 h-10 rounded-lg"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<NameAvatar
|
||||
text={getNameEnemy(locale, mapEnemy?.[enemy.id.toString()])}
|
||||
locale={locale}
|
||||
className="text-base font-semibold leading-tight truncate overflow-hidden"
|
||||
/>
|
||||
<p className="text-base-content/70 text-xs">Level {enemy.level || 1}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-xs text-base-content/70">HP:</div>
|
||||
<div className="flex text-xs font-medium">
|
||||
<div className="text-error">
|
||||
{Number(enemy?.stats?.HP ?? 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}
|
||||
</div>
|
||||
<div className="text-base-content/50 mx-1">/</div>
|
||||
<div className="text-base-content/70">
|
||||
{Number(enemy?.maxHP ?? 100).toLocaleString(undefined, { maximumFractionDigits: 0 })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full bg-base-300 rounded-full h-2.5">
|
||||
<div
|
||||
className="bg-error h-2.5 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.max(0, Math.min(100, ((enemy.stats?.HP || 0) / (enemy.maxHP || 100)) * 100))}%`
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center text-xs text-white font-medium">
|
||||
{Math.round(((enemy.stats?.HP || 0) / (enemy.maxHP || 100)) * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
"use client"
|
||||
|
||||
import useBattleDataStore from "@/stores/battleDataStore";
|
||||
import Image from "next/image";
|
||||
import useAvatarDataStore from "@/stores/avatarDataStore";
|
||||
import { getNameEnemy } from "@/helper/getNameChar";
|
||||
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import NameAvatar from "../nameAvatar";
|
||||
|
||||
export default function EnemyBar() {
|
||||
const { enemyDetail } = useBattleDataStore()
|
||||
const { mapEnemy } = useAvatarDataStore()
|
||||
const { locale } = useLocaleStore()
|
||||
|
||||
return (
|
||||
<div className="p-3 w-full">
|
||||
<div className="flex gap-3 overflow-x-auto pb-2">
|
||||
{enemyDetail && Object.values(enemyDetail).filter((enemy) => (
|
||||
enemy.stats.HP <= enemy.maxHP)).map((enemy, uid) => (
|
||||
<div key={uid} className="bg-base-200 rounded-lg p-3 border border-gray-700 w-52 shrink-0">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{mapEnemy?.[enemy.id.toString()]?.icon && (
|
||||
<Image
|
||||
src={`${process.env.CDN_URL}/${mapEnemy?.[enemy.id.toString()]?.icon}`}
|
||||
alt={enemy.name}
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
width={40}
|
||||
height={40}
|
||||
className="object-cover w-10 h-10 rounded-lg"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<NameAvatar
|
||||
text={getNameEnemy(locale, mapEnemy?.[enemy.id.toString()])}
|
||||
locale={locale}
|
||||
className="text-base font-semibold leading-tight truncate overflow-hidden"
|
||||
/>
|
||||
<p className="text-base-content/70 text-xs">Level {enemy.level || 1}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="text-xs text-base-content/70">HP:</div>
|
||||
<div className="flex text-xs font-medium">
|
||||
<div className="text-error">
|
||||
{Number(enemy?.stats?.HP ?? 0).toLocaleString(undefined, { maximumFractionDigits: 0 })}
|
||||
</div>
|
||||
<div className="text-base-content/50 mx-1">/</div>
|
||||
<div className="text-base-content/70">
|
||||
{Number(enemy?.maxHP ?? 100).toLocaleString(undefined, { maximumFractionDigits: 0 })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative w-full bg-base-300 rounded-full h-2.5">
|
||||
<div
|
||||
className="bg-error h-2.5 rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.max(0, Math.min(100, ((enemy.stats?.HP || 0) / (enemy.maxHP || 100)) * 100))}%`
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center text-xs text-white font-medium">
|
||||
{Math.round(((enemy.stats?.HP || 0) / (enemy.maxHP || 100)) * 100)}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
|
||||
export default function Footer() {
|
||||
return(
|
||||
<footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10">
|
||||
|
||||
<aside>
|
||||
<p>Copyright © {new Date().getFullYear()} - Kain (Powered by Nextjs & DaisyUi)</p>
|
||||
</aside>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Footer() {
|
||||
return(
|
||||
<footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10">
|
||||
|
||||
<aside>
|
||||
<p>Copyright © {new Date().getFullYear()} - Kain (Powered by Nextjs & DaisyUi)</p>
|
||||
</aside>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
|
||||
+498
-498
@@ -1,499 +1,499 @@
|
||||
"use client"
|
||||
import { exportBattleData, importBattleData } from "@/helper";
|
||||
import { useChangeTheme } from "@/hooks/useChangeTheme";
|
||||
import { checkConnectTcpApi } from "@/lib/api";
|
||||
import { listCurrentLanguage } from "@/lib/constant";
|
||||
import { connectSocket, disconnectSocket, getSocket, isSocketConnected } from "@/lib/socket";
|
||||
import useBattleDataStore from "@/stores/battleDataStore";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import useSocketStore from "@/stores/socketSettingStore";
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
const themes = [
|
||||
{ label: "Winter" },
|
||||
{ label: "Night" },
|
||||
{ label: "Cupcake" },
|
||||
{ label: "Coffee" },
|
||||
];
|
||||
|
||||
export default function Header() {
|
||||
const { changeTheme } = useChangeTheme()
|
||||
const { locale, setLocale } = useLocaleStore()
|
||||
const { loadBattleDataFromJSON, version } = useBattleDataStore()
|
||||
const router = useRouter()
|
||||
const transI18n = useTranslations("DataAnalysisPage")
|
||||
const { host, port, status, connectionType, setHost, setPort, setStatus, setConnectionType } = useSocketStore();
|
||||
const [message, setMessage] = useState({ text: '', type: '' });
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const cookieLocale = document.cookie.split("; ")
|
||||
.find((row) => row.startsWith("MYNEXTAPP_LOCALE"))
|
||||
?.split("=")[1];
|
||||
|
||||
if (cookieLocale) {
|
||||
if (!listCurrentLanguage.hasOwnProperty(cookieLocale)) {
|
||||
setLocale("en")
|
||||
} else {
|
||||
setLocale(cookieLocale)
|
||||
}
|
||||
|
||||
} else {
|
||||
let browserLocale = navigator.language.slice(0, 2);
|
||||
|
||||
if (!listCurrentLanguage.hasOwnProperty(browserLocale)) {
|
||||
browserLocale = "en"
|
||||
}
|
||||
setLocale(browserLocale);
|
||||
document.cookie = `MYNEXTAPP_LOCALE=${browserLocale};`
|
||||
router.refresh()
|
||||
}
|
||||
}, [router, setLocale])
|
||||
|
||||
const changeLocale = (newLocale: string) => {
|
||||
setLocale(newLocale)
|
||||
document.cookie = `MYNEXTAPP_LOCALE=${newLocale};`
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const checkStatus = () => {
|
||||
const connected = isSocketConnected();
|
||||
if (connected !== status) {
|
||||
setStatus(connected);
|
||||
}
|
||||
};
|
||||
|
||||
checkStatus();
|
||||
const intervalId = setInterval(checkStatus, 5000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [status, setStatus]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const socket = getSocket();
|
||||
|
||||
if (!socket) return;
|
||||
|
||||
return () => {
|
||||
disconnectSocket()
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleConnect = () => {
|
||||
if (!host || !port) {
|
||||
setMessage({ text: 'IP and Port are required', type: 'error' });
|
||||
return;
|
||||
}
|
||||
connectSocket();
|
||||
|
||||
setTimeout(() => {
|
||||
const connected = isSocketConnected();
|
||||
setStatus(connected);
|
||||
|
||||
if (connected) {
|
||||
setMessage({ text: 'Socket connected successfully!', type: 'success' });
|
||||
} else {
|
||||
setMessage({ text: 'Failed to connect. Please check IP and port.', type: 'error' });
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const checkConnection = async () => {
|
||||
setMessage({ text: 'Checking connection...', type: 'info' });
|
||||
let isConnect = false
|
||||
try {
|
||||
isConnect = await checkConnectTcpApi()
|
||||
} catch {
|
||||
|
||||
}
|
||||
|
||||
if (!isConnect) {
|
||||
setMessage({ text: 'Connection bridge to TCP Server failed', type: 'error' })
|
||||
} else {
|
||||
setMessage({ text: 'Connection OK!', type: 'success' });
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
};
|
||||
|
||||
// Handle ESC key to close modal
|
||||
useEffect(() => {
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isModalOpen) {
|
||||
handleCloseModal("character_detail_modal");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleEscKey);
|
||||
return () => window.removeEventListener('keydown', handleEscKey);
|
||||
}, [isModalOpen]);
|
||||
|
||||
return (
|
||||
<div className="navbar bg-base-100 shadow-md sticky top-0 z-50 px-3 py-1">
|
||||
<div className="navbar-start">
|
||||
{/* Mobile menu dropdown */}
|
||||
<div className="dropdown">
|
||||
<div tabIndex={0} role="button" className="btn btn-ghost btn-sm lg:hidden hover:bg-base-200 transition-all duration-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h8m-8 6h16" />
|
||||
</svg>
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-10 mt-3 w-52 p-2 shadow-md border border-base-200"
|
||||
>
|
||||
<li>
|
||||
<>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/json"
|
||||
id="battle-data-upload"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
importBattleData(file, loadBattleDataFromJSON)
|
||||
.then(() => console.log('Data loaded'))
|
||||
.catch(err => alert('Failed to load data: ' + err.message));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||
onClick={() => document.getElementById('battle-data-upload')?.click()}
|
||||
>
|
||||
{transI18n("loadData")}
|
||||
</button>
|
||||
</>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||
onClick={() => exportBattleData()}
|
||||
>
|
||||
{transI18n("exportData")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||
onClick={() => handleShow("socket_connection_modal")}
|
||||
>
|
||||
{transI18n("connectSetting")}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Logo */}
|
||||
|
||||
<a className="hidden sm:grid sm:grid-cols-1 items-start text-left gap-0 hover:scale-105 px-2">
|
||||
<div className="flex items-center justify-center">
|
||||
<Image src="/ff-sranalysis.png" alt="Logo" width={50} height={50} />
|
||||
<div className="flex flex-col justify-center items-start">
|
||||
<h1 className="text-xl font-bold">
|
||||
<span className="text-emerald-500">Firefly Analy</span>
|
||||
<span className="bg-clip-text text-transparent bg-linear-to-r from-emerald-400 via-orange-500 to-red-500">
|
||||
sis
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">For Veritas</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{version && (
|
||||
<div className="px-2">
|
||||
<div className="inline-flex items-center space-x-2 px-3 py-1.5 bg-linear-to-r from-blue-500 to-purple-600 rounded-full shadow-md hover:shadow-lg transition-all duration-200">
|
||||
<div className="w-1.5 h-1.5 bg-white rounded-full animate-pulse"></div>
|
||||
<div className="text-xs font-semibold text-white">
|
||||
{version}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Desktop navigation */}
|
||||
<div className="navbar-center hidden lg:flex">
|
||||
<ul className="menu menu-horizontal gap-1">
|
||||
<li>
|
||||
<>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/json"
|
||||
id="battle-data-upload"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
importBattleData(file, loadBattleDataFromJSON)
|
||||
.then(() => console.log('Data loaded'))
|
||||
.catch(err => alert('Failed to load data: ' + err.message));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||
onClick={() => document.getElementById('battle-data-upload')?.click()}
|
||||
>
|
||||
{transI18n("loadData")}
|
||||
</button>
|
||||
</>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||
onClick={() => exportBattleData()}
|
||||
>
|
||||
{transI18n("exportData")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||
onClick={() => handleShow("socket_connection_modal")}
|
||||
>
|
||||
{transI18n("connectSetting")}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Right side items */}
|
||||
<div className="navbar-end gap-2">
|
||||
<div className="px-2">
|
||||
<div className="flex items-center space-x-2 p-1.5 rounded-full shadow-md">
|
||||
<div className={`hidden lg:block text-sm italic ${status ? 'text-green-500' : 'text-red-500'}`}>
|
||||
{status ? transI18n("connected") : transI18n("unconnected")}
|
||||
</div>
|
||||
<div className={`w-3 h-3 rounded-full ${status ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Language selector - REFINED */}
|
||||
<div className="dropdown dropdown-end">
|
||||
<div className="flex items-center gap-1 border border-base-300 rounded text-sm px-1.5 py-0.5 hover:bg-base-200 cursor-pointer transition-all duration-200">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-4">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802" />
|
||||
</svg>
|
||||
|
||||
<select
|
||||
className="outline-none bg-base-200 cursor-pointer text-sm pr-0"
|
||||
value={locale}
|
||||
onChange={(e) => changeLocale(e.target.value)}
|
||||
>
|
||||
{Object.entries(listCurrentLanguage).map(([key, value]) => (
|
||||
<option key={key} value={key}>{value}</option>
|
||||
))}
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div title="Change Theme" className="dropdown dropdown-end">
|
||||
<div tabIndex={0} role="button" className="btn btn-ghost btn-sm hover:bg-base-200 transition-all duration-200 px-2">
|
||||
<svg
|
||||
width={16}
|
||||
height={16}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
className="h-4 w-4 stroke-current"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
width="10px"
|
||||
height="10px"
|
||||
className="ml-1 h-2 w-2 fill-current opacity-60"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 2048 2048"
|
||||
>
|
||||
<path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="dropdown-content bg-base-100 text-base-content rounded-lg top-px overflow-y-auto border border-base-200 shadow-md mt-12"
|
||||
>
|
||||
<ul className="menu w-40 p-1">
|
||||
{themes.map((theme) => (
|
||||
<li
|
||||
key={theme.label}
|
||||
onClick={() => {
|
||||
if (changeTheme) changeTheme(theme.label.toLowerCase());
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className="gap-2 px-2 py-1.5 hover:bg-base-200 rounded text-sm transition-all duration-200"
|
||||
data-set-theme={theme.label.toLowerCase()}
|
||||
data-act-class="[&_svg]:visible"
|
||||
>
|
||||
<div
|
||||
data-theme={theme.label.toLowerCase()}
|
||||
className="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded p-1 shadow-sm"
|
||||
>
|
||||
<div className="bg-base-content size-1 rounded-full" />
|
||||
<div className="bg-primary size-1 rounded-full" />
|
||||
<div className="bg-secondary size-1 rounded-full" />
|
||||
<div className="bg-accent size-1 rounded-full" />
|
||||
</div>
|
||||
<div className="text-sm">{theme.label}</div>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GitHub Link */}
|
||||
<Link
|
||||
className='hidden sm:flex btn btn-ghost btn-sm btn-circle bg-white/20 hover:bg-white transition-all duration-200 items-center justify-center'
|
||||
href={"https://github.com/AzenKain/SR-Analysis"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512">
|
||||
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<dialog id="socket_connection_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("socket_connection_modal")}
|
||||
>
|
||||
✕
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
||||
{transI18n("socketConnection").toUpperCase()}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4">
|
||||
{/* Select connection type */}
|
||||
<div className="form-control grid grid-cols-1 w-full mb-6">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-purple-300">{transI18n("connectionType")}</span>
|
||||
</label>
|
||||
<select
|
||||
className="select select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200"
|
||||
value={connectionType}
|
||||
onChange={(e) => setConnectionType(e.target.value)}
|
||||
>
|
||||
<option value="Native">Native</option>
|
||||
<option value="PS">PS</option>
|
||||
<option value="Other">{transI18n("other")}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Show host/port if Other */}
|
||||
{connectionType === "Other" && (
|
||||
<div className="flex flex-col md:flex-row md:space-x-4 mb-6">
|
||||
<div className="form-control w-full mb-4 md:mb-0">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-purple-300">{transI18n("host")}</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={transI18n("hostPlaceHolder")}
|
||||
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200"
|
||||
value={host}
|
||||
onChange={(e) => setHost(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-purple-300">{transI18n("port")}</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder={transI18n("portPlaceHolder")}
|
||||
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200"
|
||||
value={port || ''}
|
||||
onChange={(e) => setPort(parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message.text && (
|
||||
<div className={`alert ${message.type === 'success' ? 'alert-success' :
|
||||
message.type === 'error' ? 'alert-error' : 'alert-info'
|
||||
} mb-6`}>
|
||||
<span>{message.text}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-between items-center mt-6 mb-2">
|
||||
<div className="mb-4 sm:mb-0">
|
||||
<span className="text-md mr-2">{transI18n("status")}:</span>
|
||||
<span className={`badge ${status ? 'badge-success' : 'badge-error'} badge-lg`}>
|
||||
{status ? transI18n("connected") : transI18n("unconnected")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
className={`btn btn-primary`}
|
||||
onClick={handleConnect}
|
||||
>
|
||||
{transI18n("connect")}
|
||||
</button>
|
||||
{connectionType !== "Native" && (
|
||||
<button
|
||||
className={`btn btn-secondary`}
|
||||
onClick={checkConnection}
|
||||
>
|
||||
{transI18n("checkGameConnect")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
)
|
||||
"use client"
|
||||
import { exportBattleData, importBattleData } from "@/helper";
|
||||
import { useChangeTheme } from "@/hooks/useChangeTheme";
|
||||
import { checkConnectTcpApi } from "@/lib/api";
|
||||
import { listCurrentLanguage } from "@/lib/constant";
|
||||
import { connectSocket, disconnectSocket, getSocket, isSocketConnected } from "@/lib/socket";
|
||||
import useBattleDataStore from "@/stores/battleDataStore";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import useSocketStore from "@/stores/socketSettingStore";
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
const themes = [
|
||||
{ label: "Winter" },
|
||||
{ label: "Night" },
|
||||
{ label: "Cupcake" },
|
||||
{ label: "Coffee" },
|
||||
];
|
||||
|
||||
export default function Header() {
|
||||
const { changeTheme } = useChangeTheme()
|
||||
const { locale, setLocale } = useLocaleStore()
|
||||
const { loadBattleDataFromJSON, version } = useBattleDataStore()
|
||||
const router = useRouter()
|
||||
const transI18n = useTranslations("DataAnalysisPage")
|
||||
const { host, port, status, connectionType, setHost, setPort, setStatus, setConnectionType } = useSocketStore();
|
||||
const [message, setMessage] = useState({ text: '', type: '' });
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const cookieLocale = document.cookie.split("; ")
|
||||
.find((row) => row.startsWith("MYNEXTAPP_LOCALE"))
|
||||
?.split("=")[1];
|
||||
|
||||
if (cookieLocale) {
|
||||
if (!listCurrentLanguage.hasOwnProperty(cookieLocale)) {
|
||||
setLocale("en")
|
||||
} else {
|
||||
setLocale(cookieLocale)
|
||||
}
|
||||
|
||||
} else {
|
||||
let browserLocale = navigator.language.slice(0, 2);
|
||||
|
||||
if (!listCurrentLanguage.hasOwnProperty(browserLocale)) {
|
||||
browserLocale = "en"
|
||||
}
|
||||
setLocale(browserLocale);
|
||||
document.cookie = `MYNEXTAPP_LOCALE=${browserLocale};`
|
||||
router.refresh()
|
||||
}
|
||||
}, [router, setLocale])
|
||||
|
||||
const changeLocale = (newLocale: string) => {
|
||||
setLocale(newLocale)
|
||||
document.cookie = `MYNEXTAPP_LOCALE=${newLocale};`
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const checkStatus = () => {
|
||||
const connected = isSocketConnected();
|
||||
if (connected !== status) {
|
||||
setStatus(connected);
|
||||
}
|
||||
};
|
||||
|
||||
checkStatus();
|
||||
const intervalId = setInterval(checkStatus, 5000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, [status, setStatus]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const socket = getSocket();
|
||||
|
||||
if (!socket) return;
|
||||
|
||||
return () => {
|
||||
disconnectSocket()
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleConnect = () => {
|
||||
if (!host || !port) {
|
||||
setMessage({ text: 'IP and Port are required', type: 'error' });
|
||||
return;
|
||||
}
|
||||
connectSocket();
|
||||
|
||||
setTimeout(() => {
|
||||
const connected = isSocketConnected();
|
||||
setStatus(connected);
|
||||
|
||||
if (connected) {
|
||||
setMessage({ text: 'Socket connected successfully!', type: 'success' });
|
||||
} else {
|
||||
setMessage({ text: 'Failed to connect. Please check IP and port.', type: 'error' });
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
const checkConnection = async () => {
|
||||
setMessage({ text: 'Checking connection...', type: 'info' });
|
||||
let isConnect = false
|
||||
try {
|
||||
isConnect = await checkConnectTcpApi()
|
||||
} catch {
|
||||
|
||||
}
|
||||
|
||||
if (!isConnect) {
|
||||
setMessage({ text: 'Connection bridge to TCP Server failed', type: 'error' })
|
||||
} else {
|
||||
setMessage({ text: 'Connection OK!', type: 'success' });
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
};
|
||||
|
||||
// Handle ESC key to close modal
|
||||
useEffect(() => {
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isModalOpen) {
|
||||
handleCloseModal("character_detail_modal");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleEscKey);
|
||||
return () => window.removeEventListener('keydown', handleEscKey);
|
||||
}, [isModalOpen]);
|
||||
|
||||
return (
|
||||
<div className="navbar bg-base-100 shadow-md sticky top-0 z-50 px-3 py-1">
|
||||
<div className="navbar-start">
|
||||
{/* Mobile menu dropdown */}
|
||||
<div className="dropdown">
|
||||
<div tabIndex={0} role="button" className="btn btn-ghost btn-sm lg:hidden hover:bg-base-200 transition-all duration-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h8m-8 6h16" />
|
||||
</svg>
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-10 mt-3 w-52 p-2 shadow-md border border-base-200"
|
||||
>
|
||||
<li>
|
||||
<>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/json"
|
||||
id="battle-data-upload"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
importBattleData(file, loadBattleDataFromJSON)
|
||||
.then(() => console.log('Data loaded'))
|
||||
.catch(err => alert('Failed to load data: ' + err.message));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||
onClick={() => document.getElementById('battle-data-upload')?.click()}
|
||||
>
|
||||
{transI18n("loadData")}
|
||||
</button>
|
||||
</>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||
onClick={() => exportBattleData()}
|
||||
>
|
||||
{transI18n("exportData")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||
onClick={() => handleShow("socket_connection_modal")}
|
||||
>
|
||||
{transI18n("connectSetting")}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Logo */}
|
||||
|
||||
<a className="hidden sm:grid sm:grid-cols-1 items-start text-left gap-0 hover:scale-105 px-2">
|
||||
<div className="flex items-center justify-center">
|
||||
<Image src="/ff-sranalysis.png" alt="Logo" width={50} height={50} />
|
||||
<div className="flex flex-col justify-center items-start">
|
||||
<h1 className="text-xl font-bold">
|
||||
<span className="text-emerald-500">Firefly Analy</span>
|
||||
<span className="bg-clip-text text-transparent bg-linear-to-r from-emerald-400 via-orange-500 to-red-500">
|
||||
sis
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">For Veritas</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{version && (
|
||||
<div className="px-2">
|
||||
<div className="inline-flex items-center space-x-2 px-3 py-1.5 bg-linear-to-r from-blue-500 to-purple-600 rounded-full shadow-md hover:shadow-lg transition-all duration-200">
|
||||
<div className="w-1.5 h-1.5 bg-white rounded-full animate-pulse"></div>
|
||||
<div className="text-xs font-semibold text-white">
|
||||
{version}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* Desktop navigation */}
|
||||
<div className="navbar-center hidden lg:flex">
|
||||
<ul className="menu menu-horizontal gap-1">
|
||||
<li>
|
||||
<>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/json"
|
||||
id="battle-data-upload"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
importBattleData(file, loadBattleDataFromJSON)
|
||||
.then(() => console.log('Data loaded'))
|
||||
.catch(err => alert('Failed to load data: ' + err.message));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||
onClick={() => document.getElementById('battle-data-upload')?.click()}
|
||||
>
|
||||
{transI18n("loadData")}
|
||||
</button>
|
||||
</>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||
onClick={() => exportBattleData()}
|
||||
>
|
||||
{transI18n("exportData")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||
onClick={() => handleShow("socket_connection_modal")}
|
||||
>
|
||||
{transI18n("connectSetting")}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Right side items */}
|
||||
<div className="navbar-end gap-2">
|
||||
<div className="px-2">
|
||||
<div className="flex items-center space-x-2 p-1.5 rounded-full shadow-md">
|
||||
<div className={`hidden lg:block text-sm italic ${status ? 'text-green-500' : 'text-red-500'}`}>
|
||||
{status ? transI18n("connected") : transI18n("unconnected")}
|
||||
</div>
|
||||
<div className={`w-3 h-3 rounded-full ${status ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Language selector - REFINED */}
|
||||
<div className="dropdown dropdown-end">
|
||||
<div className="flex items-center gap-1 border border-base-300 rounded text-sm px-1.5 py-0.5 hover:bg-base-200 cursor-pointer transition-all duration-200">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-4">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802" />
|
||||
</svg>
|
||||
|
||||
<select
|
||||
className="outline-none bg-base-200 cursor-pointer text-sm pr-0"
|
||||
value={locale}
|
||||
onChange={(e) => changeLocale(e.target.value)}
|
||||
>
|
||||
{Object.entries(listCurrentLanguage).map(([key, value]) => (
|
||||
<option key={key} value={key}>{value}</option>
|
||||
))}
|
||||
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div title="Change Theme" className="dropdown dropdown-end">
|
||||
<div tabIndex={0} role="button" className="btn btn-ghost btn-sm hover:bg-base-200 transition-all duration-200 px-2">
|
||||
<svg
|
||||
width={16}
|
||||
height={16}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
className="h-4 w-4 stroke-current"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
width="10px"
|
||||
height="10px"
|
||||
className="ml-1 h-2 w-2 fill-current opacity-60"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 2048 2048"
|
||||
>
|
||||
<path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="dropdown-content bg-base-100 text-base-content rounded-lg top-px overflow-y-auto border border-base-200 shadow-md mt-12"
|
||||
>
|
||||
<ul className="menu w-40 p-1">
|
||||
{themes.map((theme) => (
|
||||
<li
|
||||
key={theme.label}
|
||||
onClick={() => {
|
||||
if (changeTheme) changeTheme(theme.label.toLowerCase());
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className="gap-2 px-2 py-1.5 hover:bg-base-200 rounded text-sm transition-all duration-200"
|
||||
data-set-theme={theme.label.toLowerCase()}
|
||||
data-act-class="[&_svg]:visible"
|
||||
>
|
||||
<div
|
||||
data-theme={theme.label.toLowerCase()}
|
||||
className="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded p-1 shadow-sm"
|
||||
>
|
||||
<div className="bg-base-content size-1 rounded-full" />
|
||||
<div className="bg-primary size-1 rounded-full" />
|
||||
<div className="bg-secondary size-1 rounded-full" />
|
||||
<div className="bg-accent size-1 rounded-full" />
|
||||
</div>
|
||||
<div className="text-sm">{theme.label}</div>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GitHub Link */}
|
||||
<Link
|
||||
className='hidden sm:flex btn btn-ghost btn-sm btn-circle bg-white/20 hover:bg-white transition-all duration-200 items-center justify-center'
|
||||
href={"https://github.com/AzenKain/SR-Analysis"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512">
|
||||
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<dialog id="socket_connection_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("socket_connection_modal")}
|
||||
>
|
||||
✕
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
||||
{transI18n("socketConnection").toUpperCase()}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4">
|
||||
{/* Select connection type */}
|
||||
<div className="form-control grid grid-cols-1 w-full mb-6">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-purple-300">{transI18n("connectionType")}</span>
|
||||
</label>
|
||||
<select
|
||||
className="select select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200"
|
||||
value={connectionType}
|
||||
onChange={(e) => setConnectionType(e.target.value)}
|
||||
>
|
||||
<option value="Native">Native</option>
|
||||
<option value="PS">PS</option>
|
||||
<option value="Other">{transI18n("other")}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Show host/port if Other */}
|
||||
{connectionType === "Other" && (
|
||||
<div className="flex flex-col md:flex-row md:space-x-4 mb-6">
|
||||
<div className="form-control w-full mb-4 md:mb-0">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-purple-300">{transI18n("host")}</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={transI18n("hostPlaceHolder")}
|
||||
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200"
|
||||
value={host}
|
||||
onChange={(e) => setHost(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-control w-full">
|
||||
<label className="label">
|
||||
<span className="label-text font-semibold text-purple-300">{transI18n("port")}</span>
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder={transI18n("portPlaceHolder")}
|
||||
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200"
|
||||
value={port || ''}
|
||||
onChange={(e) => setPort(parseInt(e.target.value) || 0)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{message.text && (
|
||||
<div className={`alert ${message.type === 'success' ? 'alert-success' :
|
||||
message.type === 'error' ? 'alert-error' : 'alert-info'
|
||||
} mb-6`}>
|
||||
<span>{message.text}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row justify-between items-center mt-6 mb-2">
|
||||
<div className="mb-4 sm:mb-0">
|
||||
<span className="text-md mr-2">{transI18n("status")}:</span>
|
||||
<span className={`badge ${status ? 'badge-success' : 'badge-error'} badge-lg`}>
|
||||
{status ? transI18n("connected") : transI18n("unconnected")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
className={`btn btn-primary`}
|
||||
onClick={handleConnect}
|
||||
>
|
||||
{transI18n("connect")}
|
||||
</button>
|
||||
{connectionType !== "Native" && (
|
||||
<button
|
||||
className={`btn btn-secondary`}
|
||||
onClick={checkConnection}
|
||||
>
|
||||
{transI18n("checkGameConnect")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+306
-306
@@ -1,307 +1,307 @@
|
||||
"use client";
|
||||
import useAvatarDataStore from "@/stores/avatarDataStore";
|
||||
import useBattleDataStore from "@/stores/battleDataStore";
|
||||
import CharacterCard from "../card/characterCard";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState, useEffect } from "react";
|
||||
import { CharacterBasic } from "@/types";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import { getNameChar } from '@/helper/getNameChar';
|
||||
import SkillBarChart from "../chart/skillBarChart";
|
||||
import SkillPieChart from "../chart/skillPieChart";
|
||||
import { motion } from "framer-motion";
|
||||
import { DamageLineForOne } from "../chart/damageLineForOne";
|
||||
import { DamagePerCycleForOne } from "../chart/damagePerCycleForOne";
|
||||
import { useCalcTotalDmgAvatar, useCalcTotalTurnAvatar } from "@/hooks/useCalcAvatarData";
|
||||
import Image from "next/image";
|
||||
import NameAvatar from "../nameAvatar";
|
||||
// import ShowCaseInfo from "../card/showCaseCard";
|
||||
|
||||
export default function LineupBar() {
|
||||
const [selectedCharacter, setSelectedCharacter] = useState<CharacterBasic | undefined>(undefined);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const { lineup, turnHistory, dataAvatar } = useBattleDataStore();
|
||||
const { listAvatar, mapAvatar } = useAvatarDataStore();
|
||||
const { locale } = useLocaleStore();
|
||||
const totalDamage = useCalcTotalDmgAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0);
|
||||
const totalTurn = useCalcTotalTurnAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0)
|
||||
|
||||
|
||||
const lineupAvatars = listAvatar?.filter(item =>
|
||||
lineup?.some(av => av?.avatarId?.toString() === item.id)
|
||||
);
|
||||
|
||||
const handleShow = (modalId: string, item: CharacterBasic) => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
setSelectedCharacter(item);
|
||||
setIsModalOpen(true);
|
||||
modal.showModal();
|
||||
}
|
||||
};
|
||||
|
||||
// Close modal handler
|
||||
const handleCloseModal = (modalId: string) => {
|
||||
setIsModalOpen(false);
|
||||
setSelectedCharacter(undefined);
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
modal.close()
|
||||
}
|
||||
};
|
||||
|
||||
// Handle ESC key to close modal
|
||||
useEffect(() => {
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isModalOpen) {
|
||||
handleCloseModal("character_detail_modal");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleEscKey);
|
||||
return () => window.removeEventListener('keydown', handleEscKey);
|
||||
}, [isModalOpen]);
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-1 rounded-lg shadow-lg w-full h-full">
|
||||
<motion.h2
|
||||
className="text-center text-xl lg:text-2xl pb-2 font-bold text-transparent bg-clip-text bg-linear-to-r from-pink-500 via-purple-500 to-cyan-500"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{transI18n("lineupInfo")}
|
||||
</motion.h2>
|
||||
|
||||
<div className="flex justify-center mb-2">
|
||||
<div className="badge badge-accent gap-2 text-lg items-center px-4 py-2 whitespace-nowrap max-w-full">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 shrink-0">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
|
||||
<span className="text-sm truncate flex flex-row">
|
||||
<div>
|
||||
{transI18n("lastTurn")}:
|
||||
</div>
|
||||
|
||||
<NameAvatar
|
||||
locale={locale}
|
||||
text={getNameChar(locale, transI18n, mapAvatar?.[turnHistory.findLast(i => i?.avatarId)?.avatarId?.toString() || ""])}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="relative h-full pt-2 max-h-[90vh] lg:max-h-[80vh] border-t-2 border-accent">
|
||||
{lineupAvatars.length === 0 ? (
|
||||
<div className="h-full flex justify-center items-start">
|
||||
<p className="text-base-content opacity-50">{transI18n("noCharactersInLineup")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full w-full overflow-x-auto md:overflow-x-hidden md:overflow-y-auto rounded-lg">
|
||||
<div className="flex flex-nowrap md:grid md:grid-cols-1 w-fit md:w-full justify-items-center items-start gap-2">
|
||||
{lineupAvatars?.map((item, index) => {
|
||||
const lastTurnAvatarId = turnHistory.findLast(i => i?.avatarId)?.avatarId || -1;
|
||||
const isLastTurn = item.id === lastTurnAvatarId.toString();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={item.id}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.1 }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
onClick={() => handleShow("character_detail_modal", item)}
|
||||
className={`cursor-pointer shrink-0 justify-items-center ${isLastTurn ? "shadow-[inset_0_0_10px_2px_rgba(59,130,246,0.7),0_0_20px_5px_rgba(59,130,246,0.3)]" : ""
|
||||
}`}
|
||||
>
|
||||
<CharacterCard data={item} />
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Character Detail Modal */}
|
||||
<dialog id="character_detail_modal" className="modal backdrop-blur-sm">
|
||||
<div className="modal-box w-11/12 max-w-7xl bg-linear-to-b bg-base-100 text-base-content border-purple-500 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("character_detail_modal")}
|
||||
>
|
||||
✕
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
||||
|
||||
<NameAvatar
|
||||
locale={locale}
|
||||
text={getNameChar(locale, transI18n, selectedCharacter).toUpperCase()}
|
||||
className={"font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedCharacter && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-6 p-2"
|
||||
>
|
||||
<div className="md:col-span-2 bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-pink-500">{transI18n("characterInformation")}</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 justify-items-start items-center gap-6">
|
||||
<p>
|
||||
{transI18n("id")}: <span className="font-bold">{selectedCharacter.id}</span>
|
||||
</p>
|
||||
<p className="flex items-center space-x-2">
|
||||
<span>{transI18n("path")}:</span>
|
||||
<span className="font-bold">{transI18n(selectedCharacter.baseType.toLowerCase())}</span>
|
||||
{selectedCharacter.baseType && (
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`/icon/${selectedCharacter.baseType.toLowerCase()}.webp`}
|
||||
className="w-6 h-6"
|
||||
alt={selectedCharacter.baseType.toLowerCase()}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{transI18n("rarity")}: <span className="font-bold">{selectedCharacter.rank === "CombatPowerAvatarRarityType5" ? "5*" : "4*"}</span>
|
||||
</p>
|
||||
<p className="flex items-center space-x-2">
|
||||
<span>{transI18n("element")}:</span>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`/icon/${selectedCharacter.damageType.toLowerCase()}.webp`}
|
||||
className="w-6 h-6"
|
||||
alt={selectedCharacter.damageType.toLowerCase()}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</p>
|
||||
|
||||
{(() => {
|
||||
const avatar = dataAvatar.find(it => it.avatar_id.toString() === selectedCharacter.id);
|
||||
if (!avatar) return null;
|
||||
const relicIds = avatar.relics.map(item => {
|
||||
const relicIdStr = item.relic_id.toString().slice(1);
|
||||
const slot = relicIdStr.slice(-1);
|
||||
return `${item.relic_set_id}_${slot}`;
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
{transI18n("level")}: <span className="font-bold">{avatar.level}</span>
|
||||
</p>
|
||||
<p>
|
||||
{transI18n("eidolons")}: <span className="font-bold">{avatar?.data?.rank}</span>
|
||||
</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>{transI18n("lightcones")}:</span>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/spriteoutput/lightconemediumicon/${avatar?.Lightcone?.item_id}.png`}
|
||||
className="w-12 h-12"
|
||||
alt={avatar?.Lightcone?.item_id?.toString() || ""}
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 w-full">
|
||||
<span>{transI18n("relics")}:</span>
|
||||
<div className="grid grid-cols-3 md:flex md:flex-row w-full">
|
||||
{relicIds.map(it => (
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
key={it}
|
||||
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${it}.png`}
|
||||
className="w-12 h-12"
|
||||
alt={avatar?.Lightcone?.item_id?.toString() || ""}
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${selectedCharacter.icon}`}
|
||||
alt={getNameChar(locale, transI18n, selectedCharacter)}
|
||||
className="h-32 w-32 object-cover rounded-full border-2 border-purple-500"
|
||||
width={128}
|
||||
height={128}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="md:col-span-2 bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-pink-500">{transI18n("characterInformation")}</h4>
|
||||
<ShowCaseInfo></ShowCaseInfo>
|
||||
</div> */}
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<p className="mt-2 font-bold text-lg text-cyan-500">{transI18n("totalTurn")}: <span className="text-base-content">{Number(totalTurn).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</span></p>
|
||||
</div>
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-purple-500">{transI18n("totalDamage")}: <span className="text-base-content">{Number(totalDamage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</span></h4>
|
||||
</div>
|
||||
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-4 text-purple-500">{transI18n("skillDamageBreakdown")}</h4>
|
||||
<SkillBarChart avatarId={Number(selectedCharacter.id) ?? 0} />
|
||||
</div>
|
||||
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h4 className="text-lg font-semibold mb-4 text-cyan-500">{transI18n("damageOverTime")}</h4>
|
||||
|
||||
</div>
|
||||
|
||||
<DamageLineForOne avatarId={Number(selectedCharacter.id) ?? 0} />
|
||||
</div>
|
||||
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-4 text-cyan-500">{transI18n("skillUsageDistribution")}</h4>
|
||||
<SkillPieChart avatarId={Number(selectedCharacter.id) ?? 0} />
|
||||
</div>
|
||||
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h4 className="text-lg font-semibold text-purple-500">{transI18n("damagePerCycle")}</h4>
|
||||
</div>
|
||||
<DamagePerCycleForOne avatarId={Number(selectedCharacter.id) ?? 0} />
|
||||
</div>
|
||||
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
"use client";
|
||||
import useAvatarDataStore from "@/stores/avatarDataStore";
|
||||
import useBattleDataStore from "@/stores/battleDataStore";
|
||||
import CharacterCard from "../card/characterCard";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useState, useEffect } from "react";
|
||||
import { CharacterBasic } from "@/types";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import { getNameChar } from '@/helper/getNameChar';
|
||||
import SkillBarChart from "../chart/skillBarChart";
|
||||
import SkillPieChart from "../chart/skillPieChart";
|
||||
import { motion } from "framer-motion";
|
||||
import { DamageLineForOne } from "../chart/damageLineForOne";
|
||||
import { DamagePerCycleForOne } from "../chart/damagePerCycleForOne";
|
||||
import { useCalcTotalDmgAvatar, useCalcTotalTurnAvatar } from "@/hooks/useCalcAvatarData";
|
||||
import Image from "next/image";
|
||||
import NameAvatar from "../nameAvatar";
|
||||
// import ShowCaseInfo from "../card/showCaseCard";
|
||||
|
||||
export default function LineupBar() {
|
||||
const [selectedCharacter, setSelectedCharacter] = useState<CharacterBasic | undefined>(undefined);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const transI18n = useTranslations("DataAnalysisPage");
|
||||
const { lineup, turnHistory, dataAvatar } = useBattleDataStore();
|
||||
const { listAvatar, mapAvatar } = useAvatarDataStore();
|
||||
const { locale } = useLocaleStore();
|
||||
const totalDamage = useCalcTotalDmgAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0);
|
||||
const totalTurn = useCalcTotalTurnAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0)
|
||||
|
||||
|
||||
const lineupAvatars = listAvatar?.filter(item =>
|
||||
lineup?.some(av => av?.avatarId?.toString() === item.id)
|
||||
);
|
||||
|
||||
const handleShow = (modalId: string, item: CharacterBasic) => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
setSelectedCharacter(item);
|
||||
setIsModalOpen(true);
|
||||
modal.showModal();
|
||||
}
|
||||
};
|
||||
|
||||
// Close modal handler
|
||||
const handleCloseModal = (modalId: string) => {
|
||||
setIsModalOpen(false);
|
||||
setSelectedCharacter(undefined);
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
modal.close()
|
||||
}
|
||||
};
|
||||
|
||||
// Handle ESC key to close modal
|
||||
useEffect(() => {
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isModalOpen) {
|
||||
handleCloseModal("character_detail_modal");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleEscKey);
|
||||
return () => window.removeEventListener('keydown', handleEscKey);
|
||||
}, [isModalOpen]);
|
||||
|
||||
return (
|
||||
<div className="p-4 md:p-1 rounded-lg shadow-lg w-full h-full">
|
||||
<motion.h2
|
||||
className="text-center text-xl lg:text-2xl pb-2 font-bold text-transparent bg-clip-text bg-linear-to-r from-pink-500 via-purple-500 to-cyan-500"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
{transI18n("lineupInfo")}
|
||||
</motion.h2>
|
||||
|
||||
<div className="flex justify-center mb-2">
|
||||
<div className="badge badge-accent gap-2 text-lg items-center px-4 py-2 whitespace-nowrap max-w-full">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6 shrink-0">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
|
||||
<span className="text-sm truncate flex flex-row">
|
||||
<div>
|
||||
{transI18n("lastTurn")}:
|
||||
</div>
|
||||
|
||||
<NameAvatar
|
||||
locale={locale}
|
||||
text={getNameChar(locale, transI18n, mapAvatar?.[turnHistory.findLast(i => i?.avatarId)?.avatarId?.toString() || ""])}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="relative h-full pt-2 max-h-[90vh] lg:max-h-[80vh] border-t-2 border-accent">
|
||||
{lineupAvatars.length === 0 ? (
|
||||
<div className="h-full flex justify-center items-start">
|
||||
<p className="text-base-content opacity-50">{transI18n("noCharactersInLineup")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full w-full overflow-x-auto md:overflow-x-hidden md:overflow-y-auto rounded-lg">
|
||||
<div className="flex flex-nowrap md:grid md:grid-cols-1 w-fit md:w-full justify-items-center items-start gap-2">
|
||||
{lineupAvatars?.map((item, index) => {
|
||||
const lastTurnAvatarId = turnHistory.findLast(i => i?.avatarId)?.avatarId || -1;
|
||||
const isLastTurn = item.id === lastTurnAvatarId.toString();
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={item.id}
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.1 }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
onClick={() => handleShow("character_detail_modal", item)}
|
||||
className={`cursor-pointer shrink-0 justify-items-center ${isLastTurn ? "shadow-[inset_0_0_10px_2px_rgba(59,130,246,0.7),0_0_20px_5px_rgba(59,130,246,0.3)]" : ""
|
||||
}`}
|
||||
>
|
||||
<CharacterCard data={item} />
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Character Detail Modal */}
|
||||
<dialog id="character_detail_modal" className="modal backdrop-blur-sm">
|
||||
<div className="modal-box w-11/12 max-w-7xl bg-linear-to-b bg-base-100 text-base-content border-purple-500 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("character_detail_modal")}
|
||||
>
|
||||
✕
|
||||
</motion.button>
|
||||
</div>
|
||||
|
||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
||||
|
||||
<NameAvatar
|
||||
locale={locale}
|
||||
text={getNameChar(locale, transI18n, selectedCharacter).toUpperCase()}
|
||||
className={"font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedCharacter && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
className="grid grid-cols-1 md:grid-cols-2 gap-6 p-2"
|
||||
>
|
||||
<div className="md:col-span-2 bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-pink-500">{transI18n("characterInformation")}</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 justify-items-start items-center gap-6">
|
||||
<p>
|
||||
{transI18n("id")}: <span className="font-bold">{selectedCharacter.id}</span>
|
||||
</p>
|
||||
<p className="flex items-center space-x-2">
|
||||
<span>{transI18n("path")}:</span>
|
||||
<span className="font-bold">{transI18n(selectedCharacter.baseType.toLowerCase())}</span>
|
||||
{selectedCharacter.baseType && (
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`/icon/${selectedCharacter.baseType.toLowerCase()}.webp`}
|
||||
className="w-6 h-6"
|
||||
alt={selectedCharacter.baseType.toLowerCase()}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{transI18n("rarity")}: <span className="font-bold">{selectedCharacter.rank === "CombatPowerAvatarRarityType5" ? "5*" : "4*"}</span>
|
||||
</p>
|
||||
<p className="flex items-center space-x-2">
|
||||
<span>{transI18n("element")}:</span>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`/icon/${selectedCharacter.damageType.toLowerCase()}.webp`}
|
||||
className="w-6 h-6"
|
||||
alt={selectedCharacter.damageType.toLowerCase()}
|
||||
width={24}
|
||||
height={24}
|
||||
/>
|
||||
</p>
|
||||
|
||||
{(() => {
|
||||
const avatar = dataAvatar.find(it => it.avatar_id.toString() === selectedCharacter.id);
|
||||
if (!avatar) return null;
|
||||
const relicIds = avatar.relics.map(item => {
|
||||
const relicIdStr = item.relic_id.toString().slice(1);
|
||||
const slot = relicIdStr.slice(-1);
|
||||
return `${item.relic_set_id}_${slot}`;
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
{transI18n("level")}: <span className="font-bold">{avatar.level}</span>
|
||||
</p>
|
||||
<p>
|
||||
{transI18n("eidolons")}: <span className="font-bold">{avatar?.data?.rank}</span>
|
||||
</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>{transI18n("lightcones")}:</span>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/spriteoutput/lightconemediumicon/${avatar?.Lightcone?.item_id}.png`}
|
||||
className="w-12 h-12"
|
||||
alt={avatar?.Lightcone?.item_id?.toString() || ""}
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 w-full">
|
||||
<span>{transI18n("relics")}:</span>
|
||||
<div className="grid grid-cols-3 md:flex md:flex-row w-full">
|
||||
{relicIds.map(it => (
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
key={it}
|
||||
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${it}.png`}
|
||||
className="w-12 h-12"
|
||||
alt={avatar?.Lightcone?.item_id?.toString() || ""}
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${selectedCharacter.icon}`}
|
||||
alt={getNameChar(locale, transI18n, selectedCharacter)}
|
||||
className="h-32 w-32 object-cover rounded-full border-2 border-purple-500"
|
||||
width={128}
|
||||
height={128}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* <div className="md:col-span-2 bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-pink-500">{transI18n("characterInformation")}</h4>
|
||||
<ShowCaseInfo></ShowCaseInfo>
|
||||
</div> */}
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<p className="mt-2 font-bold text-lg text-cyan-500">{transI18n("totalTurn")}: <span className="text-base-content">{Number(totalTurn).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</span></p>
|
||||
</div>
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-2 text-purple-500">{transI18n("totalDamage")}: <span className="text-base-content">{Number(totalDamage).toLocaleString(undefined, { minimumFractionDigits: 1, maximumFractionDigits: 1 })}</span></h4>
|
||||
</div>
|
||||
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-4 text-purple-500">{transI18n("skillDamageBreakdown")}</h4>
|
||||
<SkillBarChart avatarId={Number(selectedCharacter.id) ?? 0} />
|
||||
</div>
|
||||
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h4 className="text-lg font-semibold mb-4 text-cyan-500">{transI18n("damageOverTime")}</h4>
|
||||
|
||||
</div>
|
||||
|
||||
<DamageLineForOne avatarId={Number(selectedCharacter.id) ?? 0} />
|
||||
</div>
|
||||
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<h4 className="text-lg font-semibold mb-4 text-cyan-500">{transI18n("skillUsageDistribution")}</h4>
|
||||
<SkillPieChart avatarId={Number(selectedCharacter.id) ?? 0} />
|
||||
</div>
|
||||
|
||||
<div className="bg-base-200 rounded-lg p-4 shadow-md">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h4 className="text-lg font-semibold text-purple-500">{transI18n("damagePerCycle")}</h4>
|
||||
</div>
|
||||
<DamagePerCycleForOne avatarId={Number(selectedCharacter.id) ?? 0} />
|
||||
</div>
|
||||
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
import { parseRuby } from "@/helper";
|
||||
|
||||
|
||||
|
||||
interface TextProps {
|
||||
text: string;
|
||||
locale: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function NameAvatar({ text, locale, className }: TextProps) {
|
||||
if (locale === "ja") {
|
||||
return <div className={className} dangerouslySetInnerHTML={{ __html: parseRuby(text) }} />;
|
||||
}
|
||||
return <div className={className}>{text}</div>;
|
||||
import { parseRuby } from "@/helper";
|
||||
|
||||
|
||||
|
||||
interface TextProps {
|
||||
text: string;
|
||||
locale: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function NameAvatar({ text, locale, className }: TextProps) {
|
||||
if (locale === "ja") {
|
||||
return <div className={className} dangerouslySetInnerHTML={{ __html: parseRuby(text) }} />;
|
||||
}
|
||||
return <div className={className}>{text}</div>;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
import { PropsWithChildren, useContext } from "react";
|
||||
import { ThemeContext } from "./themeContext";
|
||||
|
||||
export function ClientThemeWrapper({ children }: PropsWithChildren) {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
return <div data-theme={theme} className="h-full">{children}</div>;
|
||||
"use client";
|
||||
import { PropsWithChildren, useContext } from "react";
|
||||
import { ThemeContext } from "./themeContext";
|
||||
|
||||
export function ClientThemeWrapper({ children }: PropsWithChildren) {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
return <div data-theme={theme} className="h-full">{children}</div>;
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./clientThemeWrapper"
|
||||
export * from "./themeContext"
|
||||
export * from "./clientThemeWrapper"
|
||||
export * from "./themeContext"
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
"use client";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import { createContext, PropsWithChildren } from "react";
|
||||
|
||||
interface ThemeContextType {
|
||||
theme?: string;
|
||||
changeTheme?: (nextTheme: string | null) => void;
|
||||
}
|
||||
export const ThemeContext = createContext<ThemeContextType>({});
|
||||
|
||||
export const ThemeProvider = ({ children }: PropsWithChildren) => {
|
||||
const { theme, setTheme } = useLocaleStore()
|
||||
|
||||
const changeTheme = (nextTheme: string | null) => {
|
||||
if (nextTheme) {
|
||||
setTheme(nextTheme);
|
||||
} else {
|
||||
setTheme(theme === "winter" ? "night" : "winter");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, changeTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
"use client";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import { createContext, PropsWithChildren } from "react";
|
||||
|
||||
interface ThemeContextType {
|
||||
theme?: string;
|
||||
changeTheme?: (nextTheme: string | null) => void;
|
||||
}
|
||||
export const ThemeContext = createContext<ThemeContextType>({});
|
||||
|
||||
export const ThemeProvider = ({ children }: PropsWithChildren) => {
|
||||
const { theme, setTheme } = useLocaleStore()
|
||||
|
||||
const changeTheme = (nextTheme: string | null) => {
|
||||
if (nextTheme) {
|
||||
setTheme(nextTheme);
|
||||
} else {
|
||||
setTheme(theme === "winter" ? "night" : "winter");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, changeTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user