diff --git a/.gitignore b/.gitignore
index 5ef6a52..c3a99b1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,8 @@
!.yarn/releases
!.yarn/versions
+/.idea
+/.history
# testing
/coverage
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/discord.xml b/.idea/discord.xml
new file mode 100644
index 0000000..30bab2a
--- /dev/null
+++ b/.idea/discord.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/firefly-analytics.iml b/.idea/firefly-analytics.iml
new file mode 100644
index 0000000..24643cc
--- /dev/null
+++ b/.idea/firefly-analytics.iml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
new file mode 100644
index 0000000..03d9549
--- /dev/null
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..563f222
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..9e26dfe
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1 @@
+{}
\ No newline at end of file
diff --git a/i18n/request.ts b/i18n/request.ts
new file mode 100644
index 0000000..bb494e3
--- /dev/null
+++ b/i18n/request.ts
@@ -0,0 +1,11 @@
+import { getRequestConfig } from "next-intl/server";
+import { cookies } from "next/headers";
+
+export default getRequestConfig( async () => {
+ const locale = (await cookies()).get("MYNEXTAPP_LOCALE")?.value || "en";
+
+ return {
+ locale,
+ messages: (await import(`../messages/${locale}.json`)).default
+ }
+})
\ No newline at end of file
diff --git a/messages/cn.json b/messages/cn.json
new file mode 100644
index 0000000..1634093
--- /dev/null
+++ b/messages/cn.json
@@ -0,0 +1,75 @@
+{
+ "TabTitle": {
+ "title": "Firefly 分析",
+ "description": "Veritas的分析工具"
+ },
+ "DataAnalysisPage": {
+ "useSkill": "使用技能",
+ "totalDamage": "总伤害",
+ "damagePerAV": "每AV伤害",
+ "totalAV": "总AV",
+ "damagerPerCycle": "每个周期的伤害",
+ "skillType": "技能类型",
+ "skillName": "技能名称",
+ "actionValue": "行动值",
+ "character": "角色",
+ "id": "ID",
+ "path": "路径",
+ "rarity": "稀有度",
+ "damageByActionValue": "根据行动值的伤害",
+ "element": "元素",
+ "totalTurn": "总回合",
+ "technique": "技巧",
+ "talent": "天赋",
+ "basic": "基础攻击",
+ "skill": "技能",
+ "ultimate": "终极技能",
+ "servant": "从者",
+ "skillDamageBreakdown": "技能伤害细分",
+ "skillUsageDistribution": "技能使用分布",
+ "damageOverTime": "持续伤害",
+ "damage": "伤害",
+ "cumulativeDamage": "累计伤害",
+ "characterInformation": "角色信息",
+ "turnDetail": "回合详情",
+ "damageDetails": "伤害详情",
+ "cycleCount": "周期数",
+ "chartInfo": "图表信息",
+ "actionBar": "行动时间线",
+ "lineupInfo": "阵容信息",
+ "loadData": "加载战斗数据",
+ "exportData": "导出战斗数据",
+ "connectSetting": "连接设置",
+ "connected": "已连接",
+ "unconnected": "未连接",
+ "socketConnection": "套接字连接",
+ "connectionType": "连接类型",
+ "status": "状态",
+ "connect": "连接",
+ "checkGameConnect": "检查游戏连接",
+ "other": "其他",
+ "host": "主机",
+ "port": "端口",
+ "hostPlaceHolder": "输入主机地址",
+ "portPlaceHolder": "输入端口号",
+ "noDamageDetail": "没有可用的伤害详情",
+ "noCharactersInLineup": "队伍中没有角色",
+ "noTurns": "尚未有回合",
+ "style": "风格",
+ "warrior": "毁灭",
+ "knight": "存护",
+ "mage": "知识",
+ "priest": "丰饶",
+ "rouge": "巡猎",
+ "shaman": "协调",
+ "warlock": "虚无",
+ "memory": "记忆",
+ "fire": "火",
+ "ice": "冰",
+ "imaginary": "虚数",
+ "physical": "物理",
+ "quantum": "量子",
+ "thunder": "雷",
+ "wind": "风"
+ }
+}
diff --git a/messages/en.json b/messages/en.json
new file mode 100644
index 0000000..7e48e06
--- /dev/null
+++ b/messages/en.json
@@ -0,0 +1,75 @@
+{
+ "TabTitle": {
+ "title": "Firely Analytics",
+ "description": "Analytics tool for Veritas"
+ },
+ "DataAnalysisPage": {
+ "useSkill": "Use skill",
+ "totalDamage": "Total damage",
+ "damagePerAV": "Damage per AV",
+ "totalAV": "Total action value",
+ "damagerPerCycle": "Damage Per Cycle",
+ "skillType": "Skill Type",
+ "skillName": "Skill Name",
+ "actionValue": "Action Value",
+ "character": "Character",
+ "id": "Id",
+ "path": "Path",
+ "rarity": "Rarity",
+ "element": "Element",
+ "totalTurn": "Total Turn",
+ "technique": "Technique",
+ "talent": "Talent",
+ "basic": "Basic Attack",
+ "skill": "Skill",
+ "ultimate": "Ultimate",
+ "servant": "Servant",
+ "skillDamageBreakdown": "Skill Damage Breakdown",
+ "skillUsageDistribution": "Skill Usage Distribution",
+ "damageOverTime": "Damage Over Time",
+ "damage": "Damage",
+ "cumulativeDamage": "Cumulative Damage",
+ "damageByActionValue": "Damage by Action Value",
+ "characterInformation": "Character Information",
+ "turnDetail": "Turn Details",
+ "damageDetails": "Damage Details",
+ "cycleCount": "Cycle Count",
+ "chartInfo": "Chart info",
+ "actionBar": "Action time line",
+ "lineupInfo": "Lineup info",
+ "loadData": "Load data battle",
+ "exportData": "Export data battle",
+ "connectSetting": "Connection Setting",
+ "connected": "Connected",
+ "unconnected": "Unconnected",
+ "socketConnection": "Socket Connection",
+ "connectionType": "Connection Type",
+ "status": "Status",
+ "connect": "Connect",
+ "checkGameConnect": "Check game connection",
+ "other": "Other",
+ "host": "Host",
+ "port": "Port",
+ "hostPlaceHolder": "Enter Host",
+ "portPlaceHolder": "Enter port number",
+ "noDamageDetail": "No damage details available",
+ "noCharactersInLineup": "No characters in lineup",
+ "noTurns": "No turns yet",
+ "style": "Style",
+ "warrior": "Destruction",
+ "knight": "Preservation",
+ "mage": "Erudition",
+ "priest": "Abundance",
+ "rouge": "The Hunt",
+ "shaman": "Harmony",
+ "warlock": "Nihility",
+ "memory": "Remembrance",
+ "fire": "Fire",
+ "ice": "Ice",
+ "imaginary": "Imaginary",
+ "physical": "Physical",
+ "quantum": "Quantum",
+ "thunder": "Thunder",
+ "wind": "Wind"
+ }
+}
diff --git a/messages/jp.json b/messages/jp.json
new file mode 100644
index 0000000..7a5a76e
--- /dev/null
+++ b/messages/jp.json
@@ -0,0 +1,75 @@
+{
+ "TabTitle": {
+ "title": "Firefly Analytics",
+ "description": "Veritasの分析ツール"
+ },
+ "DataAnalysisPage": {
+ "useSkill": "スキル使用",
+ "totalDamage": "総ダメージ",
+ "damagePerAV": "AVあたりのダメージ",
+ "totalAV": "総AV",
+ "damagerPerCycle": "サイクルごとのダメージ",
+ "skillType": "スキルタイプ",
+ "skillName": "スキル名",
+ "actionValue": "アクション値",
+ "character": "キャラクター",
+ "id": "ID",
+ "path": "パス",
+ "damageByActionValue": "行動値によるダメージ",
+ "rarity": "レアリティ",
+ "element": "エレメンタル",
+ "totalTurn": "総ターン",
+ "technique": "テクニック",
+ "talent": "タレント",
+ "basic": "基本攻撃",
+ "skill": "スキル",
+ "ultimate": "アルティメット",
+ "servant": "サーヴァント",
+ "skillDamageBreakdown": "スキルダメージ内訳",
+ "skillUsageDistribution": "スキル使用分布",
+ "damageOverTime": "時間経過によるダメージ",
+ "damage": "ダメージ",
+ "cumulativeDamage": "累積ダメージ",
+ "characterInformation": "キャラクター情報",
+ "turnDetail": "ターン詳細",
+ "damageDetails": "ダメージ詳細",
+ "cycleCount": "サイクル数",
+ "chartInfo": "チャート情報",
+ "actionBar": "アクションタイムライン",
+ "lineupInfo": "編成情報",
+ "loadData": "バトルデータをロード",
+ "exportData": "バトルデータをエクスポート",
+ "connectSetting": "接続設定",
+ "connected": "接続済み",
+ "unconnected": "接続していない",
+ "socketConnection": "ソケット接続",
+ "connectionType": "接続タイプ",
+ "status": "ステータス",
+ "connect": "接続",
+ "checkGameConnect": "ゲーム接続を確認",
+ "other": "その他",
+ "host": "ホスト",
+ "port": "ポート",
+ "hostPlaceHolder": "ホストを入力",
+ "portPlaceHolder": "ポート番号を入力",
+ "noDamageDetail": "ダメージの詳細は利用できません",
+ "noCharactersInLineup": "編成にキャラクターがいません",
+ "noTurns": "まだターンがありません",
+ "style": "スタイル",
+ "warrior": "破壊",
+ "knight": "保護",
+ "mage": "博識",
+ "priest": "豊穣",
+ "rouge": "巡狩",
+ "shaman": "調和",
+ "warlock": "虚無",
+ "memory": "記憶",
+ "fire": "炎",
+ "ice": "氷",
+ "imaginary": "想像",
+ "physical": "物理",
+ "quantum": "量子",
+ "thunder": "雷",
+ "wind": "風"
+ }
+}
diff --git a/messages/kr.json b/messages/kr.json
new file mode 100644
index 0000000..6ded2af
--- /dev/null
+++ b/messages/kr.json
@@ -0,0 +1,75 @@
+{
+ "TabTitle": {
+ "title": "Firefly 분석",
+ "description": "Veritas의 분석 도구"
+ },
+ "DataAnalysisPage": {
+ "useSkill": "스킬 사용",
+ "totalDamage": "총 피해",
+ "damagePerAV": "AV당 피해",
+ "totalAV": "총 AV",
+ "damagerPerCycle": "주기당 피해",
+ "skillType": "스킬 유형",
+ "skillName": "스킬 이름",
+ "actionValue": "액션 값",
+ "character": "캐릭터",
+ "id": "ID",
+ "path": "경로",
+ "rarity": "희귀도",
+ "damageByActionValue": "행동값에 따른 피해",
+ "element": "원소",
+ "totalTurn": "총 턴",
+ "technique": "기술",
+ "talent": "재능",
+ "basic": "기본 공격",
+ "skill": "스킬",
+ "ultimate": "궁극기",
+ "servant": "서번트",
+ "skillDamageBreakdown": "스킬 피해 분석",
+ "skillUsageDistribution": "스킬 사용 분포",
+ "damageOverTime": "시간 경과에 따른 피해",
+ "damage": "피해",
+ "cumulativeDamage": "누적 피해",
+ "characterInformation": "캐릭터 정보",
+ "turnDetail": "턴 상세",
+ "damageDetails": "피해 상세",
+ "cycleCount": "사이클 수",
+ "chartInfo": "차트 정보",
+ "actionBar": "액션 타임라인",
+ "lineupInfo": "라인업 정보",
+ "loadData": "전투 데이터 로드",
+ "exportData": "전투 데이터 내보내기",
+ "connectSetting": "연결 설정",
+ "connected": "연결됨",
+ "unconnected": "연결되지 않음",
+ "socketConnection": "소켓 연결",
+ "connectionType": "연결 유형",
+ "status": "상태",
+ "connect": "연결",
+ "checkGameConnect": "게임 연결 확인",
+ "other": "기타",
+ "host": "호스트",
+ "port": "포트",
+ "hostPlaceHolder": "호스트 입력",
+ "portPlaceHolder": "포트 번호 입력",
+ "noDamageDetail": "데미지 세부 정보가 없습니다",
+ "noCharactersInLineup": "라인업에 캐릭터가 없습니다",
+ "noTurns": "아직 턴이 없습니다",
+ "style": "스타일",
+ "warrior": "파괴",
+ "knight": "보존",
+ "mage": "지식",
+ "priest": "풍요",
+ "rouge": "사냥",
+ "shaman": "조화",
+ "warlock": "허무",
+ "memory": "기억",
+ "fire": "불",
+ "ice": "얼음",
+ "imaginary": "허수",
+ "physical": "물리",
+ "quantum": "양자",
+ "thunder": "번개",
+ "wind": "바람"
+ }
+}
diff --git a/messages/vi.json b/messages/vi.json
new file mode 100644
index 0000000..d00d4d6
--- /dev/null
+++ b/messages/vi.json
@@ -0,0 +1,75 @@
+{
+ "TabTitle": {
+ "title": "Firefly Analytics",
+ "description": "Công cụ phân tích cho Veritas"
+ },
+ "DataAnalysisPage": {
+ "useSkill": "Sử dụng kỹ năng",
+ "totalDamage": "Tổng sát thương",
+ "damagePerAV": "Sát thương mỗi AV",
+ "totalAV": "Tổng AV",
+ "damagerPerCycle": "Sát thương mỗi vòng",
+ "skillType": "Loại kỹ năng",
+ "skillName": "Tên kỹ năng",
+ "actionValue": "Giá trị hành động",
+ "character": "Nhân vật",
+ "id": "ID",
+ "path": "Đường dẫn",
+ "rarity": "Số sao",
+ "element": "Nguyên tố",
+ "totalTurn": "Tổng lượt",
+ "damageByActionValue": "Sát thương theo giá trị hành động",
+ "technique": "Bĩ kỹ",
+ "talent": "Thiên phú",
+ "basic": "Tấn công thường",
+ "skill": "Chiến kỹ",
+ "ultimate": "Tuyệt Kĩ",
+ "servant": "Vật triệu hồi",
+ "skillDamageBreakdown": "Phân tích sát thương kỹ năng",
+ "skillUsageDistribution": "Phân phối sử dụng kỹ năng",
+ "damageOverTime": "Sát thương theo thời gian",
+ "damage": "Sát thương",
+ "cumulativeDamage": "Sát thương tích lũy",
+ "characterInformation": "Thông tin nhân vật",
+ "turnDetail": "Chi tiết lượt",
+ "damageDetails": "Chi tiết sát thương",
+ "cycleCount": "Số vòng",
+ "chartInfo": "Thông tin biểu đồ",
+ "actionBar": "Thanh hành động",
+ "lineupInfo": "Thông tin đội hình",
+ "loadData": "Tải dữ liệu trận đấu",
+ "exportData": "Xuất dữ liệu trận đấu",
+ "connectSetting": "Cài đặt kết nối",
+ "connected": "Đã kết nối",
+ "unconnected": "Chưa kết nối",
+ "socketConnection": "Kết nối socket",
+ "connectionType": "Loại kết nối",
+ "status": "Trạng thái",
+ "connect": "Kết nối",
+ "checkGameConnect": "Kiểm tra kết nối game",
+ "other": "Khác",
+ "host": "Địa chỉ kết nối",
+ "port": "Cổng kết nối",
+ "hostPlaceHolder": "Nhập địa chỉ kết nối",
+ "portPlaceHolder": "Nhâp cổng kết nối",
+ "noDamageDetail": "Không có chi tiết sát thương",
+ "noCharactersInLineup": "Không có nhân vật trong đội hình",
+ "noTurns": "Chưa có lượt hành động",
+ "style": "Kiểu",
+ "warrior": "Hủy Diệt",
+ "knight": "Bảo Hộ",
+ "mage": "Tri Thức",
+ "priest": "Phong Phú",
+ "rouge": "Săn Bắn",
+ "shaman": "Hài Hòa",
+ "warlock": "Hư Vô",
+ "memory": "Ký Ức",
+ "fire": "Lửa",
+ "ice": "Băng",
+ "imaginary": "Số ảo",
+ "physical": "Vật Lý",
+ "quantum": "Lượng Tử",
+ "thunder": "Lôi",
+ "wind": "Phong"
+ }
+}
diff --git a/next.config.ts b/next.config.ts
index e9ffa30..4a5222c 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,7 +1,18 @@
+
+import createNextIntlPlugin from "next-intl/plugin";
import type { NextConfig } from "next";
+
+const withNextIntl = createNextIntlPlugin()
const nextConfig: NextConfig = {
- /* config options here */
+ async rewrites() {
+ return [
+ {
+ source: '/api/hakushin',
+ destination: 'https://api.hakush.in/hsr/data/character.json',
+ },
+ ];
+ },
};
-export default nextConfig;
+export default withNextIntl(nextConfig);
diff --git a/package-lock.json b/package-lock.json
index 25b3347..b8ce99a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,9 +8,18 @@
"name": "firefly-analytics",
"version": "0.1.0",
"dependencies": {
+ "@radix-ui/react-tooltip": "^1.2.3",
+ "chart.js": "^4.4.9",
+ "chartjs-plugin-datalabels": "^2.2.0",
+ "framer-motion": "^12.7.4",
"next": "15.3.1",
+ "next-intl": "^4.0.2",
"react": "^19.0.0",
- "react-dom": "^19.0.0"
+ "react-chartjs-2": "^5.3.0",
+ "react-dom": "^19.0.0",
+ "react-toastify": "^11.0.5",
+ "socket.io-client": "^4.8.1",
+ "zustand": "^5.0.3"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
@@ -18,6 +27,7 @@
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
+ "daisyui": "^5.0.27",
"eslint": "^9",
"eslint-config-next": "15.3.1",
"tailwindcss": "^4",
@@ -208,6 +218,104 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.6.9",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
+ "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.9"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.6.13",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
+ "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.6.0",
+ "@floating-ui/utils": "^0.2.9"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
+ "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
+ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
+ "license": "MIT"
+ },
+ "node_modules/@formatjs/ecma402-abstract": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz",
+ "integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==",
+ "license": "MIT",
+ "dependencies": {
+ "@formatjs/fast-memoize": "2.2.7",
+ "@formatjs/intl-localematcher": "0.6.1",
+ "decimal.js": "^10.4.3",
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz",
+ "integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@formatjs/fast-memoize": {
+ "version": "2.2.7",
+ "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
+ "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@formatjs/icu-messageformat-parser": {
+ "version": "2.11.2",
+ "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz",
+ "integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==",
+ "license": "MIT",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "2.3.4",
+ "@formatjs/icu-skeleton-parser": "1.8.14",
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@formatjs/icu-skeleton-parser": {
+ "version": "1.8.14",
+ "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz",
+ "integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "2.3.4",
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@formatjs/intl-localematcher": {
+ "version": "0.5.10",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz",
+ "integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -651,6 +759,12 @@
"url": "https://opencollective.com/libvips"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz",
@@ -856,6 +970,415 @@
"node": ">=12.4.0"
}
},
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz",
+ "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.4.tgz",
+ "integrity": "sha512-qz+fxrqgNxG0dYew5l7qR3c7wdgRu1XVUHGnGYX7rg5HM4p9SWaRmJwfgR3J0SgyUKayLmzQIun+N6rWRgiRKw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-dismissable-layer": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.7.tgz",
+ "integrity": "sha512-j5+WBUdhccJsmH5/H0K6RncjDtoALSEr6jbkaZu+bjw6hOPOhHycr6vEUujl+HBK8kjUfWcoCJXxP6e4lUlMZw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.0",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-escape-keydown": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-id": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
+ "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.4.tgz",
+ "integrity": "sha512-3p2Rgm/a1cK0r/UVkx5F/K9v/EplfjAeIFCGOPYPO4lZ0jtg4iSQXt/YGTSLWaf4x7NG6Z4+uKFcylcTZjeqDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.4",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.0",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-portal": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.6.tgz",
+ "integrity": "sha512-XmsIl2z1n/TsYFLIdYam2rmFwf9OC/Sh2avkbmVMDuBZIe7hSpM0cYnWPAo7nHOVx8zTuwDZGByfcqLdnzp3Vw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-presence": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.3.tgz",
+ "integrity": "sha512-IrVLIhskYhH3nLvtcBLQFZr61tBG7wx7O3kEmdzcYwRGAEBmBicGGL7ATzNgruYJ3xBTbuzEEq9OXJM3PAX3tA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.0.tgz",
+ "integrity": "sha512-/J/FhLdK0zVcILOwt5g+dH4KnkonCtkVJsa2G6JmvbbtZfBEI1gMsO3QMjseL4F/SwfAMt1Vc/0XKYKq+xJ1sw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.0.tgz",
+ "integrity": "sha512-ujc+V6r0HNDviYqIK3rW4ffgYiZ8g5DEHrGJVk4x7kTlLXRDILnKX9vAUYeIsLOoDpDJ0ujpqMkjH4w2ofuo6w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-tooltip": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.3.tgz",
+ "integrity": "sha512-0KX7jUYFA02np01Y11NWkk6Ip6TqMNmD4ijLelYAzeIndl2aVeltjJFJ2gwjNa1P8U/dgjQ+8cr9Y3Ni+ZNoRA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-dismissable-layer": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.4",
+ "@radix-ui/react-portal": "1.1.6",
+ "@radix-ui/react-presence": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.0",
+ "@radix-ui/react-slot": "1.2.0",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-visually-hidden": "1.2.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-callback-ref": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
+ "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-controllable-state": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
+ "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-effect-event": "0.0.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-effect-event": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
+ "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-escape-keydown": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
+ "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-layout-effect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
+ "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-visually-hidden": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.0.tgz",
+ "integrity": "sha512-rQj0aAWOpCdCMRbI6pLQm8r7S2BM3YhTa0SzOYD55k+hJA8oo9J+H+9wLM9oMlZWOX/wJWPTzfDfmZkf7LvCfg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
+ "license": "MIT"
+ },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -870,6 +1393,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@schummar/icu-type-parser": {
+ "version": "1.21.5",
+ "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz",
+ "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==",
+ "license": "MIT"
+ },
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+ "license": "MIT"
+ },
"node_modules/@swc/counter": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@@ -1199,7 +1734,7 @@
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz",
"integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
@@ -1209,7 +1744,7 @@
"version": "19.1.2",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz",
"integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.0.0"
@@ -2101,12 +2636,42 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/chart.js": {
+ "version": "4.4.9",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.9.tgz",
+ "integrity": "sha512-EyZ9wWKgpAU0fLJ43YAEIF8sr5F2W3LqbS40ZJyHIner2lY14ufqv2VMp69MAiZ2rpwxEUxEhIH/0U3xyRynxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
+ "node_modules/chartjs-plugin-datalabels": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-2.2.0.tgz",
+ "integrity": "sha512-14ZU30lH7n89oq+A4bWaJPnAG8a7ZTk7dKf48YAzMvJjQtjrgg5Dpk9f+LbjCF6bpx3RAGTeL13IXpKQYyRvlw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "chart.js": ">=3.0.0"
+ }
+ },
"node_modules/client-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT"
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -2178,9 +2743,19 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
+ "node_modules/daisyui": {
+ "version": "5.0.27",
+ "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.27.tgz",
+ "integrity": "sha512-XrpqgfpGaZJvTPg9pS9Rq6xbYpmMnR0a7AKqyVPZceJzjAs5HH3rfkRkiuGin0+KC2Adnu+WLHU7UDxAtCMyAw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/saadeghi/daisyui?sponsor=1"
+ }
+ },
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -2260,6 +2835,12 @@
}
}
},
+ "node_modules/decimal.js": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
+ "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
+ "license": "MIT"
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -2348,6 +2929,45 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/engine.io-client": {
+ "version": "6.6.3",
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz",
+ "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.17.1",
+ "xmlhttprequest-ssl": "~2.1.1"
+ }
+ },
+ "node_modules/engine.io-client/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
@@ -3116,6 +3736,33 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/framer-motion": {
+ "version": "12.7.4",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.7.4.tgz",
+ "integrity": "sha512-jX0bPsTmU0oPZTYz/dVyD0dmOyEOEJvdn0TaZBE5I8g2GvVnnQnW9f65cJnoVfUkY3WZWNXGXnPbVA9YnaIfVA==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.7.4",
+ "motion-utils": "^12.7.2",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -3443,6 +4090,18 @@
"node": ">= 0.4"
}
},
+ "node_modules/intl-messageformat": {
+ "version": "10.7.16",
+ "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz",
+ "integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "2.3.4",
+ "@formatjs/fast-memoize": "2.2.7",
+ "@formatjs/icu-messageformat-parser": "2.11.2",
+ "tslib": "^2.8.0"
+ }
+ },
"node_modules/is-array-buffer": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz",
@@ -4326,11 +4985,25 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/motion-dom": {
+ "version": "12.7.4",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.7.4.tgz",
+ "integrity": "sha512-1ZUHAoSUMMxP6jPqyxlk9XUfb6NxMsnWPnH2YGhrOhTURLcXWbETi6eemoKb60Pe32NVJYduL4B62VQSO5Jq8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.7.2"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.7.2",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.7.2.tgz",
+ "integrity": "sha512-XhZwqctxyJs89oX00zn3OGCuIIpVevbTa+u82usWBC6pSHUd2AoNWiYa7Du8tJxJy9TFbZ82pcn5t7NOm1PHAw==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@@ -4377,6 +5050,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/next": {
"version": "15.3.1",
"resolved": "https://registry.npmjs.org/next/-/next-15.3.1.tgz",
@@ -4431,6 +5113,33 @@
}
}
},
+ "node_modules/next-intl": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.0.2.tgz",
+ "integrity": "sha512-3cKVflwdrqxCOvAL+DtGN68qR802i0PEj0dttkAD5IK5XxOjugQs4yU8aSakvPMbkOrhEJ+89z5lG2EAqi7Gkw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/amannn"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@formatjs/intl-localematcher": "^0.5.4",
+ "negotiator": "^1.0.0",
+ "use-intl": "^4.0.2"
+ },
+ "peerDependencies": {
+ "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0",
+ "typescript": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -4810,6 +5519,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/react-chartjs-2": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.0.tgz",
+ "integrity": "sha512-UfZZFnDsERI3c3CZGxzvNJd02SHjaSJ8kgW1djn65H1KK8rehwTjyrRKOG3VTMG8wtHZ5rgAO5oTHtHi9GCCmw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "chart.js": "^4.1.1",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/react-dom": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
@@ -4829,6 +5548,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/react-toastify": {
+ "version": "11.0.5",
+ "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-11.0.5.tgz",
+ "integrity": "sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==",
+ "license": "MIT",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19",
+ "react-dom": "^18 || ^19"
+ }
+ },
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -5222,6 +5954,68 @@
"is-arrayish": "^0.3.1"
}
},
+ "node_modules/socket.io-client": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
+ "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.2",
+ "engine.io-client": "~6.6.1",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-client/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
+ "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.3.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io-parser/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -5633,7 +6427,7 @@
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -5711,6 +6505,20 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-intl": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.0.2.tgz",
+ "integrity": "sha512-6RAP/5KJMRzLMLS25/BVh2u09cRK8S6HRGc1RnZvqR547qAKZCpjYylOqMPU9eNIirAiKoGmsoUPa7JrlaA/yg==",
+ "license": "MIT",
+ "dependencies": {
+ "@formatjs/fast-memoize": "^2.2.0",
+ "@schummar/icu-type-parser": "1.21.5",
+ "intl-messageformat": "^10.5.14"
+ },
+ "peerDependencies": {
+ "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -5826,6 +6634,35 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ws": {
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
+ "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xmlhttprequest-ssl": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
+ "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@@ -5838,6 +6675,35 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zustand": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
+ "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
}
}
}
diff --git a/package.json b/package.json
index b326674..2565e55 100644
--- a/package.json
+++ b/package.json
@@ -9,19 +9,29 @@
"lint": "next lint"
},
"dependencies": {
+ "@radix-ui/react-tooltip": "^1.2.3",
+ "chart.js": "^4.4.9",
+ "chartjs-plugin-datalabels": "^2.2.0",
+ "framer-motion": "^12.7.4",
+ "next": "15.3.1",
+ "next-intl": "^4.0.2",
"react": "^19.0.0",
+ "react-chartjs-2": "^5.3.0",
"react-dom": "^19.0.0",
- "next": "15.3.1"
+ "react-toastify": "^11.0.5",
+ "socket.io-client": "^4.8.1",
+ "zustand": "^5.0.3"
},
"devDependencies": {
- "typescript": "^5",
+ "@eslint/eslintrc": "^3",
+ "@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
- "@tailwindcss/postcss": "^4",
- "tailwindcss": "^4",
+ "daisyui": "^5.0.27",
"eslint": "^9",
"eslint-config-next": "15.3.1",
- "@eslint/eslintrc": "^3"
+ "tailwindcss": "^4",
+ "typescript": "^5"
}
}
diff --git a/src/app/favicon.ico b/src/app/favicon.ico
index 718d6fe..b8133f5 100644
Binary files a/src/app/favicon.ico and b/src/app/favicon.ico differ
diff --git a/src/app/globals.css b/src/app/globals.css
index a2dc41e..3549807 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,26 +1,4 @@
@import "tailwindcss";
-
-:root {
- --background: #ffffff;
- --foreground: #171717;
-}
-
-@theme inline {
- --color-background: var(--background);
- --color-foreground: var(--foreground);
- --font-sans: var(--font-geist-sans);
- --font-mono: var(--font-geist-mono);
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --background: #0a0a0a;
- --foreground: #ededed;
- }
-}
-
-body {
- background: var(--background);
- color: var(--foreground);
- font-family: Arial, Helvetica, sans-serif;
+@plugin "daisyui" {
+ themes: winter --default, night --prefersdark, cupcake, coffee;
}
diff --git a/src/app/icon.png b/src/app/icon.png
new file mode 100644
index 0000000..02ef1c8
Binary files /dev/null and b/src/app/icon.png differ
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f7fa87e..2295b12 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,6 +1,12 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
+import Header from "../components/header";
+import {ClientThemeWrapper, ThemeProvider} from "@/components/themeController";
+import Footer from "@/components/footer";
+import { NextIntlClientProvider } from "next-intl";
+import { getLocale, getMessages } from "next-intl/server";
+import { ToastContainer } from 'react-toastify';
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -13,21 +19,35 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
- title: "Create Next App",
- description: "Generated by create next app",
+ title: "Firefly Analytics",
+ description: "Analytics tool for Veritas",
};
-export default function RootLayout({
+export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
+ const messages = await getMessages();
+ const locale = await getLocale()
return (
-
+
- {children}
+
+
+
+
+
+ {children}
+
+
+
+
+
+
);
diff --git a/src/app/page.tsx b/src/app/page.tsx
index e68abe6..d7af526 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,103 +1,184 @@
-import Image from "next/image";
+"use client";
+import { useTranslations } from "next-intl";
+import { useEffect, useState } from "react";
+import ActionBar from "@/components/actionbar";
+import useAvatarDataStore from "@/stores/avatarDataStore";
+import { getCharacterListApi } from "@/lib/api";
+import LineupBar from "@/components/lineupbar";
+import useBattleDataStore from "@/stores/battleDataStore";
+import DamagePerAvatarForAll from "@/components/chart/damagePerAvatarForAll";
+import MultiCharLineChart from "@/components/chart/damageLineForAll";
+import DamagePerCycleForAll from "@/components/chart/damagePerCycleForAll";
+import DamagePercentChartForAll from "@/components/chart/damagePercentForAll";
export default function Home() {
- return (
-
-
-
-
- -
- Get started by editing{" "}
-
- src/app/page.tsx
-
- .
-
- -
- Save and see your changes instantly.
-
-
+ const transI18n = useTranslations("DataAnalysisPage");
+ const { setListAvatar } = useAvatarDataStore();
+ const {
+ totalAV,
+ totalDamage,
+ damagePerAV,
+ } = useBattleDataStore();
+ const [modeBar, setModeBar] = useState<0 | 1 | 2>(1);
+ const [modeLine, setModeLine] = useState<0 | 1>(1);
+ const [expandedCharts, setExpandedCharts] = useState([]);
-
-
-
- Deploy now
-
-
- Read our docs
-
+ const toggleExpand = (chartId: string) => {
+ if (expandedCharts.includes(chartId.toLowerCase())) {
+ setExpandedCharts(expandedCharts.filter(id => id !== chartId.toLowerCase()));
+ } else {
+ setExpandedCharts([...expandedCharts, chartId.toLowerCase()]);
+ }
+ };
+
+ useEffect(() => {
+ const fetchData = async () => {
+ const data = await getCharacterListApi();
+ setListAvatar(data);
+ };
+ fetchData();
+ }, [setListAvatar]);
+
+ useEffect(() => {
+ window.dispatchEvent(new Event('resize'));
+ }, [expandedCharts]);
+
+ return (
+
+
+
+
+
+
+
+
+ {transI18n("totalDamage")}: {Number(totalDamage).toFixed(2)}
+
+
+ {transI18n("totalAV")}: {Number(totalAV).toFixed(2)}
+
+
+ {transI18n("damagePerAV")}: {Number(damagePerAV).toFixed(2)}
+
+
+
+
+
+
+ {expandedCharts.includes('chart1') ? (
+
+
+
+
+
+
+ ) : (
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ {[0, 1].map((m) => (
+
+ ))}
+
+
+
+
+ {expandedCharts.includes('chart3') ? (
+
+
+
+
+
+
+ ) : (
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ {[0, 1, 2].map((m) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
-
-
+
);
}
diff --git a/src/components/actionbar/index.tsx b/src/components/actionbar/index.tsx
new file mode 100644
index 0000000..356d434
--- /dev/null
+++ b/src/components/actionbar/index.tsx
@@ -0,0 +1,260 @@
+"use client";
+import useAvatarDataStore from "@/stores/avatarDataStore";
+import useBattleDataStore from "@/stores/battleDataStore";
+import useLocaleStore from "@/stores/localeStore";
+import { AvatarType } from "@/types";
+import { TurnBattleInfo } from "@/types/mics";
+import { useEffect, useState, useRef } from "react";
+import { useTranslations } from "next-intl";
+import { motion } from "framer-motion";
+import { getNameChar } from "@/helper";
+
+export default function ActionBar() {
+ const [selectTurn, setSelectTurn] = useState
(null);
+ const [selectAvatar, setSelectAvatar] = useState(null);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const { turnHistory } = useBattleDataStore();
+ const { listAvatar } = useAvatarDataStore();
+ const { locale } = useLocaleStore();
+ const transI18n = useTranslations("DataAnalysisPage");
+ const turnListRef = useRef(null);
+
+ const parallelogramStyle: React.CSSProperties = {
+ transform: 'skew(-9deg)',
+ overflow: 'hidden',
+ };
+
+ const contentStyle: React.CSSProperties = {
+ transform: 'skew(9deg)',
+ display: 'flex',
+ flexDirection: 'column',
+ alignItems: 'center',
+ };
+
+ const handleShow = (modalId: string, avatar: AvatarType, turn: TurnBattleInfo) => {
+ 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 && turnHistory.length > 0) {
+ turnListRef.current.scrollTop = turnListRef.current.scrollHeight;
+ }
+ }, [turnHistory.length]);
+
+ return (
+
+
+ {transI18n("actionBar")}
+
+
+
+
+
+ {turnHistory.length === 0 ? (
+
+
{transI18n("noTurns")}
+
+ ) : (
+ turnHistory.map((turn, index) => {
+ const data = listAvatar.find(it => it.id === turn.avatarId.toString());
+ if (!data) return null;
+ const text = getNameChar(locale, data);
+
+ return (
+
+
handleShow("action_detail_modal", data, turn)}
+ style={parallelogramStyle}
+ className="flex border bg-base-100 w-full hover:bg-base-200 transition-colors duration-200 border-cyan-400 border-l-4 cursor-pointer"
+ >
+
+
+
+

+
+
+
{text}
+
+
+
+ {`${transI18n("useSkill")}: ${turn.skillType}`}
+
+
+ {`${transI18n("totalDamage")}: ${turn.totalDamage.toFixed(2)}`}
+
+
+
+
+ );
+ })
+ )}
+
+
+
+ {/* Character Detail Modal */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/card/characterCard.tsx b/src/components/card/characterCard.tsx
new file mode 100644
index 0000000..b611780
--- /dev/null
+++ b/src/components/card/characterCard.tsx
@@ -0,0 +1,66 @@
+"use client";
+
+import { getNameChar } from '@/helper';
+import useLocaleStore from '@/stores/localeStore';
+import { AvatarType } from '@/types';
+
+interface CharacterCardProps {
+ data: AvatarType
+}
+
+export function parseRuby(text: string): string {
+ return text.replace(/\{RUBY_B#(.*?)\}(.*?)\{RUBY_E#\}/g, (_, furigana, kanji) => {
+ return `${kanji}`;
+ });
+}
+
+
+export default function CharacterCard({ data }: CharacterCardProps) {
+ const { locale } = useLocaleStore();
+ const text = getNameChar(locale, data)
+ return (
+
+
+
+ {locale === "jp" ? (
+
+ ) : (
+
+ {text}
+
+ )}
+
+
+ );
+}
diff --git a/src/components/chart/damageLineForAll.tsx b/src/components/chart/damageLineForAll.tsx
new file mode 100644
index 0000000..ff666c4
--- /dev/null
+++ b/src/components/chart/damageLineForAll.tsx
@@ -0,0 +1,65 @@
+'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';
+
+ChartJS.register(LineElement, CategoryScale, LinearScale, PointElement, Tooltip, Legend);
+
+const colors = ['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c'];
+
+export default function MultiCharLineChart({ mode = 0 }: { mode?: 0 | 1 }) {
+ const dataByAvatar = useDamageLinesForAll(mode);
+ const avatarIds = Object.keys(dataByAvatar).map(Number);
+ const { listAvatar } = useAvatarDataStore()
+ const { locale } = useLocaleStore();
+
+ const data = {
+ datasets: avatarIds.map((id, idx) => ({
+ label: getNameChar(locale, listAvatar.find(it => it.id == 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,
+ },
+ },
+ plugins: {
+ datalabels: {
+ display: false,
+ },
+ },
+ };
+
+ return ;
+}
diff --git a/src/components/chart/damageLineForOne.tsx b/src/components/chart/damageLineForOne.tsx
new file mode 100644
index 0000000..5e47195
--- /dev/null
+++ b/src/components/chart/damageLineForOne.tsx
@@ -0,0 +1,67 @@
+"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";
+
+ChartJS.register(
+ LineElement,
+ PointElement,
+ CategoryScale,
+ LinearScale,
+ Title,
+ Tooltip,
+ Legend,
+ Filler
+);
+
+ChartJS.defaults.set('plugins.datalabels', {
+ color: '#FE777B'
+});
+
+export function DamageLineForOne({ avatarId, mode = 0 }: { avatarId: number; mode?: 0 | 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 },
+ datalabels: {
+ display: false,
+ },
+ },
+ scales: {
+ x: { title: { display: true, text: transI18n("actionValue") } },
+ y: { title: { display: true, text: transI18n("damage") }, beginAtZero: true },
+ },
+
+ };
+
+ return ;
+}
diff --git a/src/components/chart/damagePerAvatarForAll.tsx b/src/components/chart/damagePerAvatarForAll.tsx
new file mode 100644
index 0000000..fba67cf
--- /dev/null
+++ b/src/components/chart/damagePerAvatarForAll.tsx
@@ -0,0 +1,80 @@
+"use client";
+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 { useTranslations } from 'next-intl';
+import { Bar } from 'react-chartjs-2';
+
+
+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)',
+];
+
+const borderPalette = colorPalette.map((color) => color.replace('0.6', '1'));
+
+export default function DamagePerAvatarForAll() {
+ const transI18n = useTranslations("DataAnalysisPage");
+ const { lineup, turnHistory } = useBattleDataStore()
+ const { listAvatar } = useAvatarDataStore()
+ const { locale } = useLocaleStore();
+ const damageByAvatar = lineup.map((avatar) => {
+ const turns = turnHistory.filter((turn) => turn.avatarId === avatar.avatarId);
+ const totalDmg = turns.reduce((sum, turn) => sum + turn.totalDamage, 0);
+ const char = listAvatar.find(it => it.id === avatar.avatarId.toString())
+ if (!char) return
+ return {
+ avatarId: avatar.avatarId,
+ damage: totalDmg,
+ avatarName: getNameChar(locale, char)
+ };
+ });
+
+ const data = {
+ labels: damageByAvatar.map((item) => item?.avatarName),
+ datasets: [
+ {
+ label: transI18n("totalDamage"),
+ data: damageByAvatar.map((item) => item?.damage),
+ backgroundColor: damageByAvatar.map((_, idx) => colorPalette[idx % colorPalette.length]),
+ borderColor: damageByAvatar.map((_, idx) => borderPalette[idx % borderPalette.length]),
+ borderWidth: 1,
+ },
+ ],
+ };
+
+ const options = {
+ responsive: true,
+ scales: {
+ y: {
+ beginAtZero: true,
+ ticks: {
+ precision: 0,
+ },
+ },
+ },
+ plugins: {
+ datalabels: {
+ display: false,
+ },
+ },
+ };
+
+ return ;
+}
diff --git a/src/components/chart/damagePerCycleForAll.tsx b/src/components/chart/damagePerCycleForAll.tsx
new file mode 100644
index 0000000..68968f4
--- /dev/null
+++ b/src/components/chart/damagePerCycleForAll.tsx
@@ -0,0 +1,44 @@
+"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";
+
+ChartJS.register(BarElement, ArcElement, RadialLinearScale);
+export default function DamagePerCycleForAll({
+ mode,
+}: {
+ mode: 0 | 1 | 2;
+}) {
+ const dataRaw = useDamagePerCycleForAll(mode);
+ const transI18n = useTranslations("DataAnalysisPage");
+ const data = {
+ labels: dataRaw.map(d => d.x),
+ datasets: [
+ {
+ label: mode === 0 ? `${transI18n("damagerPerCycle")} (100av)` : mode === 1 ? `${transI18n("damagerPerCycle")} (150av | 100v)` : `${transI18n("damagerPerCycle")} (150av | 150av | 100v)`,
+ data: dataRaw.map(d => d.y),
+ backgroundColor: "rgba(255,99,132,0.6)",
+ },
+ ],
+ };
+
+ const options: ChartOptions<"bar"> = {
+ responsive: true,
+ plugins: {
+ legend: { display: true },
+ datalabels: {
+ display: false,
+ },
+ },
+ scales: {
+ x: { title: { display: true, text: transI18n("cycleCount") } },
+ y: { title: { display: true, text: transI18n("totalDamage") }, beginAtZero: true },
+ },
+
+ };
+
+ return ;
+}
diff --git a/src/components/chart/damagePerCycleForOne.tsx b/src/components/chart/damagePerCycleForOne.tsx
new file mode 100644
index 0000000..24e6e93
--- /dev/null
+++ b/src/components/chart/damagePerCycleForOne.tsx
@@ -0,0 +1,46 @@
+"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";
+
+ChartJS.register(BarElement, ArcElement, RadialLinearScale);
+export function DamagePerCycleForOne({
+ avatarId,
+ mode,
+}: {
+ avatarId: number;
+ mode: 0 | 1 | 2;
+}) {
+ const dataRaw = useDamagePerCycleForOne(avatarId, mode);
+ const transI18n = useTranslations("DataAnalysisPage");
+ const data = {
+ labels: dataRaw.map(d => d.x),
+ datasets: [
+ {
+ label: mode === 0 ? `${transI18n("damagerPerCycle")} (100av)` : mode === 1 ? `${transI18n("damagerPerCycle")} (150av | 100v)` : `${transI18n("damagerPerCycle")} (150av | 150av | 100v)`,
+ data: dataRaw.map(d => d.y),
+ backgroundColor: "rgba(255,99,132,0.6)",
+ },
+ ],
+ };
+
+ const options: ChartOptions<"bar"> = {
+ responsive: true,
+ plugins: {
+ legend: { display: true },
+ datalabels: {
+ display: false,
+ },
+ },
+ scales: {
+ x: { title: { display: true, text: transI18n("cycleCount") } },
+ y: { title: { display: true, text: transI18n("totalDamage") }, beginAtZero: true },
+ },
+
+ };
+
+ return ;
+}
diff --git a/src/components/chart/damagePercentForAll.tsx b/src/components/chart/damagePercentForAll.tsx
new file mode 100644
index 0000000..d625756
--- /dev/null
+++ b/src/components/chart/damagePercentForAll.tsx
@@ -0,0 +1,51 @@
+'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';
+
+ChartJS.register(ArcElement, Tooltip, Legend);
+
+const colors = ['#f87171', '#34d399', '#60a5fa', '#facc15', '#a78bfa', '#fb923c', '#f472b6'];
+
+export default function DamagePercentChartForAll() {
+ const data = useDamagePercentPerAvatar();
+ const { listAvatar } = useAvatarDataStore()
+ const { locale } = useLocaleStore();
+ const transI18n = useTranslations("DataAnalysisPage");
+ const chartData = {
+ labels: data.map(d => getNameChar(locale, listAvatar.find(it => it.id == d.avatarId.toString()))),
+ datasets: [
+ {
+ label: '% ' + transI18n("damage"),
+ data: data.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 ;
+}
diff --git a/src/components/chart/skillBarChart.tsx b/src/components/chart/skillBarChart.tsx
new file mode 100644
index 0000000..0919aad
--- /dev/null
+++ b/src/components/chart/skillBarChart.tsx
@@ -0,0 +1,58 @@
+'use client';
+import { getNameChar } from '@/helper';
+import { useSkillDamageByAvatar } from '@/hooks/useSkillDamageStats';
+import useAvatarDataStore from '@/stores/avatarDataStore';
+import useLocaleStore from '@/stores/localeStore';
+import {
+ Chart as ChartJS,
+ BarElement,
+ CategoryScale,
+ LinearScale,
+ Tooltip,
+ Legend,
+} from 'chart.js';
+import ChartDataLabels from 'chartjs-plugin-datalabels';
+import { useTranslations } from 'next-intl';
+import { Bar } from 'react-chartjs-2';
+
+ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Legend, ChartDataLabels);
+
+export default function SkillBarChart({ avatarId }: { avatarId: number }) {
+ const { labels, damageValues } = useSkillDamageByAvatar(avatarId);
+ const transI18n = useTranslations("DataAnalysisPage");
+ const colors = ['#f87171', '#facc15', '#34d399', '#60a5fa', '#a78bfa', '#fb923c'];
+ const { listAvatar } = useAvatarDataStore()
+ const { locale } = useLocaleStore();
+ const data = {
+ labels: labels.map(it => transI18n(it.toLowerCase())),
+ datasets: [
+ {
+ label: getNameChar(locale, listAvatar.find(it => it.id === avatarId.toString())),
+ data: damageValues,
+ backgroundColor: colors.slice(0, labels.length),
+ borderColor: '#111',
+ borderWidth: 1,
+ },
+ ],
+ };
+
+ const options = {
+ responsive: true,
+ scales: {
+ y: {
+ beginAtZero: true,
+ ticks: { precision: 0 },
+ },
+ },
+ plugins: {
+ legend: {
+ display: false,
+ },
+ datalabels: {
+ display: false,
+ },
+ },
+ };
+
+ return ;
+}
diff --git a/src/components/chart/skillPieChart.tsx b/src/components/chart/skillPieChart.tsx
new file mode 100644
index 0000000..64e2a8e
--- /dev/null
+++ b/src/components/chart/skillPieChart.tsx
@@ -0,0 +1,59 @@
+"use client";
+import { useSkillDamageByAvatar } from '@/hooks/useSkillDamageStats';
+import {
+ Chart as ChartJS,
+ ArcElement,
+ Tooltip,
+ Legend,
+} from 'chart.js';
+import { Pie } from 'react-chartjs-2';
+import ChartDataLabels from 'chartjs-plugin-datalabels';
+import { useTranslations } from 'next-intl';
+
+ChartJS.register(ArcElement, Tooltip, Legend, ChartDataLabels);
+
+export default function SkillPieChart({ avatarId }: { avatarId: number }) {
+ const { labels, damageValues } = useSkillDamageByAvatar(avatarId);
+ const transI18n = useTranslations("DataAnalysisPage");
+ 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: [
+ '#f87171', // red
+ '#facc15', // yellow
+ '#34d399', // green
+ '#60a5fa', // blue
+ '#a78bfa', // purple
+ '#fb923c', // orange
+ ],
+ borderColor: '#fff',
+ borderWidth: 2,
+ },
+ ],
+ };
+
+ return (
+
+ );
+}
diff --git a/src/components/footer/index.tsx b/src/components/footer/index.tsx
new file mode 100644
index 0000000..b3372bd
--- /dev/null
+++ b/src/components/footer/index.tsx
@@ -0,0 +1,11 @@
+
+export default function Footer() {
+ return(
+
+ )
+}
diff --git a/src/components/header/index.tsx b/src/components/header/index.tsx
new file mode 100644
index 0000000..32e7d6f
--- /dev/null
+++ b/src/components/header/index.tsx
@@ -0,0 +1,459 @@
+"use client"
+import { exportBattleData, importBattleData } from "@/helper";
+import { useChangeTheme } from "@/hooks/useChangeTheme";
+import { checkConnectTcpApi } from "@/lib/api";
+import { connectSocket, disconnectSocket, getSocket, isSocketConnected } from "@/lib/socket";
+import useBattleDataStore from "@/stores/battleDataStore";
+import useLocaleStore from "@/stores/localeStore";
+import useSocketStore from "@/stores/socketSettingStore";
+import { BattleDataStateJson } from "@/types/mics";
+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";
+
+const themes = [
+ { label: "Winter" },
+ { label: "Night" },
+ { label: "Cupcake" },
+ { label: "Coffee" },
+];
+
+export default function Header() {
+ const { changeTheme } = useChangeTheme()
+ const { locale, setLocale } = useLocaleStore()
+ const {
+ totalAV,
+ totalDamage,
+ damagePerAV,
+ turnHistory,
+ lineup,
+ loadBattleDataFromJSON,
+ } = 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) {
+ setLocale(cookieLocale)
+ } else {
+ const browserLocale = navigator.language.slice(0, 2);
+ 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()
+ };
+ }, [setStatus]);
+
+ 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 (
+
+
+ {/* Mobile menu dropdown */}
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+ {/* Logo */}
+
+
+
+ Firefly Analy
+
+ sis
+
+
+ For Veritas
+
+
+
+
+ {/* Desktop navigation */}
+
+
+
+ {/* Right side items */}
+
+
+
+
+ {status ? transI18n("connected") : transI18n("unconnected")}
+
+
+
+
+ {/* Language selector - REFINED */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {themes.map((theme) => (
+ - {
+ if (changeTheme) changeTheme(theme.label.toLowerCase());
+ }}
+ >
+
+
+ ))}
+
+
+
+
+ {/* GitHub Link */}
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/lineupbar/index.tsx b/src/components/lineupbar/index.tsx
new file mode 100644
index 0000000..5cd06c0
--- /dev/null
+++ b/src/components/lineupbar/index.tsx
@@ -0,0 +1,274 @@
+"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 { AvatarType } 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";
+
+export default function LineupBar() {
+ const [selectedCharacter, setSelectedCharacter] = useState(null);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ const transI18n = useTranslations("DataAnalysisPage");
+ const { lineup } = useBattleDataStore();
+ const { listAvatar } = useAvatarDataStore();
+ const { locale } = useLocaleStore();
+ const [modeBar, setModeBar] = useState<0 | 1 | 2>(1);
+ const [modeLine, setModeLine] = useState<0 | 1>(1);
+
+
+ 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: AvatarType) => {
+ 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(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("character_detail_modal");
+ }
+ };
+
+ window.addEventListener('keydown', handleEscKey);
+ return () => window.removeEventListener('keydown', handleEscKey);
+ }, [isModalOpen]);
+
+ return (
+
+
+ {transI18n("lineupInfo")}
+
+
+
+ {lineupAvatars.length === 0 ? (
+
+
{transI18n("noCharactersInLineup")}
+
+ ) : (
+
+
+
+ {lineupAvatars.map((item, index) => (
+ handleShow("character_detail_modal", item)}
+ >
+
+
+ ))}
+
+
+ )}
+
+ {/* Character Detail Modal */}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/themeController/clientThemeWrapper.tsx b/src/components/themeController/clientThemeWrapper.tsx
new file mode 100644
index 0000000..704e8f5
--- /dev/null
+++ b/src/components/themeController/clientThemeWrapper.tsx
@@ -0,0 +1,8 @@
+"use client";
+import { PropsWithChildren, useContext } from "react";
+import { ThemeContext } from "./themeContext";
+
+export function ClientThemeWrapper({ children }: PropsWithChildren) {
+ const { theme } = useContext(ThemeContext);
+ return {children}
;
+}
\ No newline at end of file
diff --git a/src/components/themeController/index.ts b/src/components/themeController/index.ts
new file mode 100644
index 0000000..5600303
--- /dev/null
+++ b/src/components/themeController/index.ts
@@ -0,0 +1,2 @@
+export * from "./clientThemeWrapper"
+export * from "./themeContext"
diff --git a/src/components/themeController/themeContext.tsx b/src/components/themeController/themeContext.tsx
new file mode 100644
index 0000000..18d3ee9
--- /dev/null
+++ b/src/components/themeController/themeContext.tsx
@@ -0,0 +1,41 @@
+"use client";
+import { createContext, PropsWithChildren, useEffect, useState } from "react";
+
+interface ThemeContextType {
+ theme?: string;
+ changeTheme?: (nextTheme: string | null) => void;
+}
+export const ThemeContext = createContext({});
+
+export const ThemeProvider = ({ children }: PropsWithChildren) => {
+ const [theme, setTheme] = useState("light");
+
+ useEffect(() => {
+ if (typeof window !== "undefined") {
+ const storedTheme = localStorage.getItem("theme");
+ if (storedTheme) setTheme(storedTheme);
+ }
+ }, []);
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const changeTheme = (nextTheme: string | null) => {
+ console.log(nextTheme)
+ if (nextTheme) {
+ setTheme(nextTheme);
+ if (typeof window !== "undefined") {
+ localStorage.setItem("theme", nextTheme);
+ }
+ } else {
+ setTheme((prev) => (prev === "light" ? "night" : "light"));
+ if (typeof window !== "undefined") {
+ localStorage.setItem("theme", theme);
+ }
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+};
\ No newline at end of file
diff --git a/src/helper/exportDataBattle.ts b/src/helper/exportDataBattle.ts
new file mode 100644
index 0000000..0d819b5
--- /dev/null
+++ b/src/helper/exportDataBattle.ts
@@ -0,0 +1,15 @@
+import { BattleDataStateJson } from "@/types/mics";
+
+export const exportBattleData = (
+ data: BattleDataStateJson,
+ filename = 'battle_data.json'
+ ) => {
+ const dataStr = JSON.stringify(data, null, 2);
+ const blob = new Blob([dataStr], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = filename;
+ a.click();
+ URL.revokeObjectURL(url);
+ };
\ No newline at end of file
diff --git a/src/helper/getNameChar.ts b/src/helper/getNameChar.ts
new file mode 100644
index 0000000..b71cff9
--- /dev/null
+++ b/src/helper/getNameChar.ts
@@ -0,0 +1,19 @@
+import { AvatarType } from "@/types";
+
+
+export function getNameChar(locale: string, data: AvatarType | undefined): string {
+ if (!data) {
+ return ""
+ }
+ let text = data.lang.get(locale) ?? "";
+ if (!text) {
+ text = data.lang.get("en") ?? "";
+ }
+ if (Number(data.id) % 2 === 0 && Number(data.id) > 8000) {
+ text = `Female ${data.damageType} MC`
+ } else if (Number(data.id) > 8000) {
+ text = `Male ${data.damageType} MC`
+ }
+
+ return text
+}
diff --git a/src/helper/importDataBattle.ts b/src/helper/importDataBattle.ts
new file mode 100644
index 0000000..ccefa99
--- /dev/null
+++ b/src/helper/importDataBattle.ts
@@ -0,0 +1,21 @@
+import { BattleDataStateJson } from "@/types/mics";
+
+export const importBattleData = (file: File, loadBattleDataFromJSON: (data: BattleDataStateJson) => void) => {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ try {
+ const result = reader.result as string;
+ const parsed = JSON.parse(result) as BattleDataStateJson;
+ loadBattleDataFromJSON(parsed);
+ resolve();
+ } catch (err) {
+ reject(err);
+ }
+ };
+ reader.onerror = () => reject(reader.error);
+ reader.readAsText(file);
+ });
+};
+
+
\ No newline at end of file
diff --git a/src/helper/index.ts b/src/helper/index.ts
new file mode 100644
index 0000000..f1cf26d
--- /dev/null
+++ b/src/helper/index.ts
@@ -0,0 +1,3 @@
+export * from "./getNameChar"
+export * from "./exportDataBattle"
+export * from "./importDataBattle"
\ No newline at end of file
diff --git a/src/hooks/useCalcAvatarData.ts b/src/hooks/useCalcAvatarData.ts
new file mode 100644
index 0000000..b4f98a4
--- /dev/null
+++ b/src/hooks/useCalcAvatarData.ts
@@ -0,0 +1,28 @@
+import useBattleDataStore from "@/stores/battleDataStore";
+import { useMemo } from "react";
+
+export function useCalcTotalDmgAvatar(avatarId: number) {
+ const { turnHistory } = useBattleDataStore.getState();
+
+ return useMemo(() => {
+ return turnHistory
+ .filter(t => t.avatarId === avatarId)
+ .reduce((sum, turn) => sum + turn.totalDamage, 0);
+ }, [avatarId, turnHistory]);
+}
+
+
+export function useCalcTotalTurnAvatar(avatarId: number) {
+ const { turnHistory } = useBattleDataStore.getState();
+
+ return useMemo(() => {
+ const uniqueActionValues = new Set();
+ turnHistory.forEach(turn => {
+ if (turn.avatarId === avatarId) {
+ uniqueActionValues.add(turn.actionValue);
+ }
+ });
+
+ return uniqueActionValues.size;
+ }, [avatarId, turnHistory]);
+ }
\ No newline at end of file
diff --git a/src/hooks/useChangeTheme.ts b/src/hooks/useChangeTheme.ts
new file mode 100644
index 0000000..9007ded
--- /dev/null
+++ b/src/hooks/useChangeTheme.ts
@@ -0,0 +1,4 @@
+import { useContext } from "react";
+import { ThemeContext } from "@/components/themeController";
+
+export const useChangeTheme = () => useContext(ThemeContext);
\ No newline at end of file
diff --git a/src/hooks/useDamageLineForOne.ts b/src/hooks/useDamageLineForOne.ts
new file mode 100644
index 0000000..34e8cba
--- /dev/null
+++ b/src/hooks/useDamageLineForOne.ts
@@ -0,0 +1,31 @@
+import useBattleDataStore from "@/stores/battleDataStore";
+import { useMemo } from "react";
+
+export function useDamageLineForOne(avatarId: number, mode: 0 | 1 = 0) {
+ const { turnHistory } = useBattleDataStore.getState();
+
+ return useMemo(() => {
+ const map = new Map();
+
+ for (const turn of turnHistory) {
+ if (turn.avatarId !== avatarId) continue;
+
+ const prev = map.get(turn.actionValue) || 0;
+ map.set(turn.actionValue, prev + turn.totalDamage);
+ }
+
+ const points = Array.from(map.entries())
+ .map(([x, y]) => ({ x, y }))
+ .sort((a, b) => a.x - b.x);
+
+ if (mode === 1) {
+ let cumulative = 0;
+ return points.map(p => {
+ cumulative += p.y;
+ return { x: p.x, y: Number(cumulative.toFixed(2)) };
+ });
+ }
+
+ return points.map(p => ({ x: p.x, y: Number(p.y.toFixed(2)) }));
+ }, [avatarId, turnHistory, mode]);
+}
diff --git a/src/hooks/useDamageLinesForAll.ts b/src/hooks/useDamageLinesForAll.ts
new file mode 100644
index 0000000..787e5cb
--- /dev/null
+++ b/src/hooks/useDamageLinesForAll.ts
@@ -0,0 +1,43 @@
+import useBattleDataStore from "@/stores/battleDataStore";
+import { useMemo } from "react";
+
+export function useDamageLinesForAll(mode: 0 | 1 = 0) {
+ const { turnHistory } = useBattleDataStore.getState();
+
+ return useMemo(() => {
+ const avatarMap = new Map>();
+
+ for (const turn of turnHistory) {
+ if (!avatarMap.has(turn.avatarId)) {
+ avatarMap.set(turn.avatarId, new Map());
+ }
+
+ const charMap = avatarMap.get(turn.avatarId)!;
+ const prev = charMap.get(turn.actionValue) || 0;
+ charMap.set(turn.actionValue, prev + turn.totalDamage);
+ }
+
+ const result: Record = {};
+
+ for (const [avatarId, damageMap] of avatarMap.entries()) {
+ const points = Array.from(damageMap.entries())
+ .map(([x, y]) => ({ x, y }))
+ .sort((a, b) => a.x - b.x);
+
+ if (mode === 1) {
+ let cumulative = 0;
+ result[avatarId] = points.map(p => {
+ cumulative += p.y;
+ return { x: p.x, y: Number(cumulative.toFixed(2)) };
+ });
+ } else {
+ result[avatarId] = points.map(p => ({
+ x: p.x,
+ y: Number(p.y.toFixed(2)),
+ }));
+ }
+ }
+
+ return result;
+ }, [turnHistory, mode]);
+}
diff --git a/src/hooks/useDamagePerCycle.ts b/src/hooks/useDamagePerCycle.ts
new file mode 100644
index 0000000..d9e489f
--- /dev/null
+++ b/src/hooks/useDamagePerCycle.ts
@@ -0,0 +1,79 @@
+import useBattleDataStore from "@/stores/battleDataStore";
+import { useMemo } from "react";
+
+
+type Mode = 0 | 1 | 2;
+
+function getChunkIndex(av: number, mode: Mode): number {
+ let sum = 0;
+ let idx = 0;
+
+ if (mode === 0) {
+ while (sum <= av) {
+ sum += 100;
+ idx++;
+ }
+ } else if (mode === 1) {
+ if (av < 150) return 0;
+ sum = 150;
+ idx = 1;
+ while (sum <= av) {
+ sum += 100;
+ idx++;
+ }
+ } else {
+ if (av < 150) return 0;
+ if (av < 300) return 1;
+ sum = 300;
+ idx = 2;
+ while (sum <= av) {
+ sum += 100;
+ idx++;
+ }
+ }
+
+ return idx - 1;
+}
+
+export function useDamagePerCycleForOne(avatarId: number, mode: Mode) {
+ const { turnHistory } = useBattleDataStore.getState();
+
+ return useMemo(() => {
+ const damageMap = new Map();
+
+ turnHistory
+ .filter(t => t.avatarId === avatarId)
+ .forEach(t => {
+ const idx = getChunkIndex(t.actionValue, mode);
+ damageMap.set(idx, (damageMap.get(idx) || 0) + t.totalDamage);
+ });
+
+ const maxIndex = Math.max(...Array.from(damageMap.keys()), 0);
+
+ return Array.from({ length: maxIndex + 1 }).map((_, i) => ({
+ x: `${i + 1}`,
+ y: damageMap.get(i) || 0,
+ }));
+ }, [avatarId, mode, turnHistory]);
+}
+
+
+export function useDamagePerCycleForAll(mode: Mode) {
+ const { turnHistory } = useBattleDataStore.getState();
+
+ return useMemo(() => {
+ const damageMap = new Map();
+
+ turnHistory.forEach(t => {
+ const idx = getChunkIndex(t.actionValue, mode);
+ damageMap.set(idx, (damageMap.get(idx) || 0) + t.totalDamage);
+ });
+
+ const maxIndex = Math.max(...Array.from(damageMap.keys()), 0);
+
+ return Array.from({ length: maxIndex + 1 }).map((_, i) => ({
+ x: `${i + 1}`,
+ y: damageMap.get(i) || 0,
+ }));
+ }, [mode, turnHistory]);
+}
\ No newline at end of file
diff --git a/src/hooks/useDamagePercentPerAvatar.ts b/src/hooks/useDamagePercentPerAvatar.ts
new file mode 100644
index 0000000..72757a6
--- /dev/null
+++ b/src/hooks/useDamagePercentPerAvatar.ts
@@ -0,0 +1,21 @@
+import useBattleDataStore from "@/stores/battleDataStore";
+import { useMemo } from "react";
+
+export function useDamagePercentPerAvatar() {
+ const { turnHistory } = useBattleDataStore.getState();
+
+ return useMemo(() => {
+ const dmgByAvatar = new Map();
+
+ turnHistory.forEach(t => {
+ dmgByAvatar.set(t.avatarId, (dmgByAvatar.get(t.avatarId) || 0) + t.totalDamage);
+ });
+
+ const totalDmg = Array.from(dmgByAvatar.values()).reduce((sum, v) => sum + v, 0);
+
+ return Array.from(dmgByAvatar.entries()).map(([avatarId, dmg]) => ({
+ avatarId,
+ percent: totalDmg > 0 ? (dmg / totalDmg) * 100 : 0,
+ }));
+ }, [turnHistory]);
+}
diff --git a/src/hooks/useSkillDamageStats.ts b/src/hooks/useSkillDamageStats.ts
new file mode 100644
index 0000000..60371b1
--- /dev/null
+++ b/src/hooks/useSkillDamageStats.ts
@@ -0,0 +1,25 @@
+"use client";
+import { useMemo } from "react";
+import useBattleDataStore from "@/stores/battleDataStore";
+
+export function useSkillDamageByAvatar(avatarId: number) {
+ const { turnHistory } = useBattleDataStore.getState();
+
+ return useMemo(() => {
+ const skillTypes = ['technique', 'talent', 'basic', 'skill', 'ultimate', 'servant'];
+ const dmgMap = new Map();
+ skillTypes.forEach((type) => dmgMap.set(type, 0));
+
+ for (const turn of turnHistory) {
+ const type = turn.skillType.toLowerCase();
+ if (turn.avatarId === avatarId && dmgMap.has(type)) {
+ dmgMap.set(type, dmgMap.get(type)! + turn.totalDamage);
+ }
+ }
+
+ const labels = Array.from(dmgMap.keys());
+ const damageValues = Array.from(dmgMap.values());
+
+ return { labels, damageValues };
+ }, [avatarId, turnHistory]);
+}
diff --git a/src/lib/api.ts b/src/lib/api.ts
new file mode 100644
index 0000000..61e17b6
--- /dev/null
+++ b/src/lib/api.ts
@@ -0,0 +1,65 @@
+import useSocketStore from "@/stores/socketSettingStore";
+import { AvatarHakushiType, AvatarType } from "@/types/avatar";
+
+
+export async function checkConnectTcpApi(): Promise {
+ const { host, port, connectionType } = useSocketStore.getState()
+ let url = `${host}:${port}/check-tcp`
+ if (connectionType === "FireflyPSLocal") {
+ url = "http://localhost:21000/check-tcp"
+ }
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+ return data.status
+ }
+ return false
+}
+
+export async function getCharacterListApi(): Promise {
+ const res = await fetch('/api/hakushin', {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!res.ok) {
+ console.log(`Error ${res.status}: ${res.statusText}`);
+ return [];
+ }
+
+ const data: Map = new Map(Object.entries(await res.json()));
+
+
+ return Array.from(data.entries()).map(([id, it]) => convertAvatar(id, it));
+}
+
+
+function convertAvatar(id: string, item: AvatarHakushiType): AvatarType {
+ const lang = new Map([
+ ['en', item.en],
+ ['kr', item.kr],
+ ['cn', item.cn],
+ ['jp', item.jp]
+ ]);
+
+ const result: AvatarType = {
+ release: item.release,
+ icon: item.icon,
+ rank: item.rank,
+ baseType: item.baseType,
+ damageType: item.damageType,
+ desc: item.desc,
+ lang: lang,
+ id: id
+ };
+
+ return result;
+}
\ No newline at end of file
diff --git a/src/lib/socket.ts b/src/lib/socket.ts
new file mode 100644
index 0000000..7b3bbcb
--- /dev/null
+++ b/src/lib/socket.ts
@@ -0,0 +1,125 @@
+import { io, Socket } from "socket.io-client";
+import useSocketStore from "@/stores/socketSettingStore";
+import { toast } from 'react-toastify';
+import useBattleDataStore from "@/stores/battleDataStore";
+
+let socket: Socket | null = null;
+
+
+const onBattleBegin = () => {
+ notify("Battle Started!", "info")
+}
+
+const notify = (msg: string, type: 'info' | 'success' | 'error' = 'info') => {
+ if (type === 'success') toast.success(msg);
+ else if (type === 'error') toast.error(msg);
+ else toast.info(msg);
+};
+
+export const connectSocket = (): Socket => {
+ const { host, port, connectionType, setStatus } = useSocketStore.getState();
+ const {
+ onSetBattleLineupService,
+ onTurnEndService,
+ onUseSkillService,
+ onKillService,
+ onDamageService,
+ onBattleEndService,
+ onTurnBeginService
+ } = useBattleDataStore.getState();
+
+ let url = `${host}:${port}`;
+ if (connectionType === "FireflyPSLocal") {
+ url = "http://localhost:21000"
+ }
+
+ if (socket) {
+ socket.disconnect();
+ }
+
+ socket = io(url, {
+ reconnectionAttempts: 5,
+ timeout: 10000,
+ autoConnect: true,
+ });
+
+ socket.on("connect", () => {
+ console.log("Socket connected");
+ setStatus(true);
+ });
+
+ socket.on("disconnect", () => {
+ console.log("Socket disconnected");
+ setStatus(false);
+ });
+
+ socket.on("connect_error", (err) => {
+ console.error("Connection error:", err);
+ setStatus(false);
+ });
+
+ socket.on("connect_timeout", () => {
+ console.warn("Connection timeout");
+ setStatus(false);
+ });
+
+ socket.on("reconnect_failed", () => {
+ console.error("Reconnect failed");
+ setStatus(false);
+ });
+
+ const onConnect = () => {
+ setStatus(true);
+ notify(`Kết nối thành công với Socket ID: ${socket?.id}`, 'success');
+ };
+
+ if (isSocketConnected()) onConnect();
+
+ socket.on("SetBattleLineup", (json) => onSetBattleLineupService(JSON.parse(json)));
+ socket.on("TurnEnd", (json) => onTurnEndService(JSON.parse(json)));
+ socket.on("OnUseSkill", (json) => onUseSkillService(JSON.parse(json)));
+ socket.on("OnKill", (json) => onKillService(JSON.parse(json)));
+ socket.on("OnDamage", (json) => onDamageService(JSON.parse(json)));
+ socket.on('BattleBegin', () => onBattleBegin());
+ socket.on('TurnBegin', (json) => onTurnBeginService(JSON.parse(json)));
+ socket.on('BattleEnd', (json) => onBattleEndService(JSON.parse(json)));
+
+ socket.on("Error", (msg: string) => {
+ console.error("Server Error:", msg);
+ });
+
+ return socket;
+};
+
+export const disconnectSocket = (): void => {
+ const {
+ onSetBattleLineupService,
+ onTurnEndService,
+ onUseSkillService,
+ onKillService,
+ onDamageService,
+ onBattleEndService,
+ onTurnBeginService
+ } = useBattleDataStore.getState();
+ if (socket) {
+ socket.off("SetBattleLineup", (json) => onSetBattleLineupService(JSON.parse(json)));
+ socket.off("TurnEnd", (json) => onTurnEndService(JSON.parse(json)));
+ socket.off("OnUseSkill", (json) => onUseSkillService(JSON.parse(json)));
+ socket.off("OnKill", (json) => onKillService(JSON.parse(json)));
+ socket.off("OnDamage", (json) => onDamageService(JSON.parse(json)));
+ socket.off('BattleBegin', () => onBattleBegin());
+ socket.off('TurnBegin', (json) => onTurnBeginService(JSON.parse(json)));
+ socket.off('BattleEnd', (json) => onBattleEndService(JSON.parse(json)));
+ socket.offAny();
+ socket.disconnect();
+ useSocketStore.getState().setStatus(false);
+ }
+};
+
+export const isSocketConnected = (): boolean => {
+ return socket?.connected || false;
+};
+
+export const getSocket = (): Socket | null => {
+ return socket;
+};
diff --git a/src/stores/avatarDataStore.ts b/src/stores/avatarDataStore.ts
new file mode 100644
index 0000000..020a3ce
--- /dev/null
+++ b/src/stores/avatarDataStore.ts
@@ -0,0 +1,16 @@
+import { AvatarType } from '@/types';
+import { create } from 'zustand'
+
+
+interface AvatarDataState {
+ listAvatar: AvatarType[];
+ setListAvatar: (list: AvatarType[]) => void;
+}
+
+const useAvatarDataStore = create((set) => ({
+ listAvatar: [],
+ setListAvatar: (list: AvatarType[]) => set({ listAvatar: list }),
+
+}));
+
+export default useAvatarDataStore;
\ No newline at end of file
diff --git a/src/stores/battleDataStore.ts b/src/stores/battleDataStore.ts
new file mode 100644
index 0000000..3204b15
--- /dev/null
+++ b/src/stores/battleDataStore.ts
@@ -0,0 +1,129 @@
+import { AttackResultType, AvatarSkillType, BattleEndType, KillType, LineUpType, TurnBeginType, TurnEndType } from '@/types';
+import { AvatarBattleInfo, BattleDataStateJson, TurnBattleInfo } from '@/types/mics';
+import { create } from 'zustand'
+
+
+interface BattleDataState {
+ lineup: AvatarBattleInfo[];
+ turnHistory: TurnBattleInfo[]
+ totalAV: number;
+ totalDamage: number;
+ damagePerAV: number;
+
+ onSetBattleLineupService: (data: LineUpType) => void;
+ onTurnEndService: (data: TurnEndType) => void;
+ onBattleEndService: (data: BattleEndType) => void;
+ onUseSkillService: (data: AvatarSkillType) => void;
+ onKillService: (data: KillType) => void
+ onDamageService: (data: AttackResultType) => void;
+ onBattleStartService: () => void;
+ onTurnBeginService: (data: TurnBeginType) => void;
+ loadBattleDataFromJSON: (data: BattleDataStateJson) => void;
+}
+
+const useBattleDataStore = create((set, get) => ({
+ lineup: [],
+ turnHistory: [],
+ totalAV: 0,
+ totalDamage: 0,
+ damagePerAV: 0,
+
+ loadBattleDataFromJSON: (data: BattleDataStateJson) => {
+ set({
+ lineup: data.lineup,
+ turnHistory: data.turnHistory,
+ totalAV: data.totalAV,
+ totalDamage: data.totalDamage,
+ damagePerAV: data.damagePerAV,
+ })
+ },
+ onBattleStartService: () => {
+ set({
+ lineup: [],
+ turnHistory: [],
+ totalAV: 0,
+ totalDamage: 0,
+ damagePerAV: 0,
+ })
+ },
+ onDamageService: (data: AttackResultType) => {
+ const turnHistorys = get().turnHistory
+ const turnIdx = turnHistorys.findLastIndex(it => it.avatarId === data.attacker.id)
+ if (turnIdx === -1) {
+ return
+ }
+ const newTh = [...turnHistorys]
+ newTh[turnIdx].damageDetail.push(data.damage)
+ newTh[turnIdx].totalDamage += data.damage
+ set({
+ turnHistory: newTh,
+ totalDamage: get().totalDamage + data.damage,
+ damagePerAV: (get().totalDamage + data.damage) / (get().totalAV === 0 ? 1 : get().totalAV)
+ })
+ },
+
+ onKillService: (data: KillType) => {
+ const lineups = get().lineup
+ const avatarIdx = lineups.findIndex(it => it.avatarId === data.attacker.id)
+ if (avatarIdx === -1) {
+ return
+ }
+ const newLn = [...lineups]
+ newLn[avatarIdx].isDie = true
+ set({
+ lineup: newLn
+ })
+ },
+ onSetBattleLineupService: (data: LineUpType) => {
+ const lineups: AvatarBattleInfo[] = []
+ for (const avatar of data.avatars) {
+ lineups.push({ avatarId: avatar.id, isDie: false } as AvatarBattleInfo)
+ }
+ set({
+ lineup: lineups,
+ turnHistory: [],
+ totalAV: 0,
+ totalDamage: 0,
+ damagePerAV: 0,
+ });
+ },
+ onTurnBeginService: (data: TurnBeginType) => {
+ set((state) => ({
+ totalAV: data.action_value,
+ damagePerAV: state.totalDamage / (data.action_value === 0 ? 1 : data.action_value)
+ }))
+ },
+ onTurnEndService: (data: TurnEndType) => {
+ set((state) => ({
+ totalDamage: state.totalDamage === data.total_damage ? data.total_damage : state.totalDamage,
+ totalAV: data.action_value,
+ damagePerAV: (state.totalDamage === data.total_damage ? data.total_damage : state.totalDamage) / (data.action_value === 0 ? 1 : data.action_value)
+ }));
+ },
+ onUseSkillService: (data: AvatarSkillType) => {
+ set((state) => ({
+ turnHistory: [...state.turnHistory, {
+ avatarId: data.avatar.id,
+ damageDetail: [],
+ totalDamage: 0,
+ actionValue: state.totalAV,
+ skillType: data.skill.type,
+ skillName: data.skill.name
+ } as TurnBattleInfo]
+ }))
+ },
+ onBattleEndService: (data: BattleEndType) => {
+ const lineups: AvatarBattleInfo[] = []
+ for (const avatar of data.avatars) {
+ lineups.push({ avatarId: avatar.id, isDie: false } as AvatarBattleInfo)
+ }
+ set({
+ lineup: lineups,
+ totalDamage: data.total_damage,
+ totalAV: data.action_value,
+ damagePerAV: data.total_damage / (data.action_value === 0 ? 1 : data.action_value)
+ })
+ }
+}));
+
+export default useBattleDataStore;
\ No newline at end of file
diff --git a/src/stores/localeStore.ts b/src/stores/localeStore.ts
new file mode 100644
index 0000000..fe1a742
--- /dev/null
+++ b/src/stores/localeStore.ts
@@ -0,0 +1,15 @@
+import { create } from 'zustand'
+
+
+interface LocaleState {
+ locale: string;
+ setLocale: (newLocale: string) => void;
+}
+
+const useLocaleStore = create((set) => ({
+ locale: "en",
+ setLocale: (newLocale: string) => set({ locale: newLocale }),
+
+}));
+
+export default useLocaleStore;
\ No newline at end of file
diff --git a/src/stores/socketSettingStore.ts b/src/stores/socketSettingStore.ts
new file mode 100644
index 0000000..006061e
--- /dev/null
+++ b/src/stores/socketSettingStore.ts
@@ -0,0 +1,33 @@
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+interface SocketState {
+ host: string;
+ port: number;
+ status: boolean;
+ connectionType: string;
+ setHost: (host: string) => void;
+ setConnectionType: (host: string) => void;
+ setPort: (port: number) => void;
+ setStatus: (status: boolean) => void;
+}
+
+const useSocketStore = create()(
+ persist(
+ (set) => ({
+ host: "http://localhost",
+ port: 3443,
+ status: false,
+ connectionType: "FireflyPSLocal",
+ setHost: (host: string) => set({ host }),
+ setConnectionType: (connectionType: string) => set({ connectionType }),
+ setPort: (port: number) => set({ port }),
+ setStatus: (status: boolean) => set({ status }),
+ }),
+ {
+ name: 'socket-storage',
+ }
+ )
+);
+
+export default useSocketStore;
diff --git a/src/types/attack.ts b/src/types/attack.ts
new file mode 100644
index 0000000..2445920
--- /dev/null
+++ b/src/types/attack.ts
@@ -0,0 +1,6 @@
+import { AvatarInfo } from "./lineup";
+
+export interface AttackResultType {
+ attacker: AvatarInfo;
+ damage: number;
+}
\ No newline at end of file
diff --git a/src/types/avatar.ts b/src/types/avatar.ts
new file mode 100644
index 0000000..efa1096
--- /dev/null
+++ b/src/types/avatar.ts
@@ -0,0 +1,23 @@
+export interface AvatarHakushiType {
+ release: number;
+ icon: string;
+ rank: string;
+ baseType: string;
+ damageType: string;
+ en: string;
+ desc: string;
+ kr: string;
+ cn: string;
+ jp: string;
+}
+
+export interface AvatarType {
+ id: string;
+ release: number;
+ icon: string;
+ rank: string;
+ baseType: string;
+ damageType: string;
+ desc: string;
+ lang: Map;
+}
diff --git a/src/types/battle.ts b/src/types/battle.ts
new file mode 100644
index 0000000..fb7e8a7
--- /dev/null
+++ b/src/types/battle.ts
@@ -0,0 +1,14 @@
+import { AvatarInfo } from "./lineup";
+import { TurnInfo } from "./turn";
+
+export interface BattleEndType {
+ avatars: AvatarInfo[];
+ turn_history: TurnInfo[];
+ turn_count: number;
+ total_damage: number;
+ action_value: number;
+}
+export interface KillType {
+ attacker: AvatarInfo;
+}
+
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..70e35be
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,6 @@
+export * from "./attack"
+export * from "./avatar"
+export * from "./battle"
+export * from "./lineup"
+export * from "./skill"
+export * from "./turn"
\ No newline at end of file
diff --git a/src/types/lineup.ts b/src/types/lineup.ts
new file mode 100644
index 0000000..edd7687
--- /dev/null
+++ b/src/types/lineup.ts
@@ -0,0 +1,8 @@
+export interface AvatarInfo {
+ id: number;
+ name: string;
+}
+
+export interface LineUpType {
+ avatars: AvatarInfo[];
+}
\ No newline at end of file
diff --git a/src/types/mics.ts b/src/types/mics.ts
new file mode 100644
index 0000000..b3180ab
--- /dev/null
+++ b/src/types/mics.ts
@@ -0,0 +1,21 @@
+export interface AvatarBattleInfo {
+ avatarId: number;
+ isDie: boolean;
+}
+
+export interface TurnBattleInfo {
+ avatarId: number;
+ damageDetail: number[];
+ totalDamage: number;
+ actionValue: number;
+ skillType: string;
+ skillName: string;
+}
+
+export interface BattleDataStateJson {
+ lineup: AvatarBattleInfo[];
+ turnHistory: TurnBattleInfo[]
+ totalAV: number;
+ totalDamage: number;
+ damagePerAV: number;
+}
\ No newline at end of file
diff --git a/src/types/skill.ts b/src/types/skill.ts
new file mode 100644
index 0000000..8025a79
--- /dev/null
+++ b/src/types/skill.ts
@@ -0,0 +1,11 @@
+import { AvatarInfo } from "./lineup";
+
+export interface SkillInfo {
+ name: string;
+ type: string;
+}
+
+export interface AvatarSkillType {
+ avatar: AvatarInfo;
+ skill: SkillInfo;
+}
\ No newline at end of file
diff --git a/src/types/turn.ts b/src/types/turn.ts
new file mode 100644
index 0000000..b73528a
--- /dev/null
+++ b/src/types/turn.ts
@@ -0,0 +1,19 @@
+import { AvatarInfo } from "./lineup";
+
+
+export interface TurnInfo {
+ avatars_turn_damage: number[];
+ total_damage: number;
+ action_value: number;
+}
+
+export interface TurnBeginType {
+ action_value: number;
+}
+
+export interface TurnEndType {
+ avatars: AvatarInfo[];
+ avatars_damage: number[];
+ total_damage: number;
+ action_value: number;
+}
\ No newline at end of file