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 ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
+ 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([]); -
- - Vercel logomark - 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} +
+
+
{text}
+
+
+
+ {`${transI18n("useSkill")}: ${turn.skillType}`} +
+
+ {`${transI18n("totalDamage")}: ${turn.totalDamage.toFixed(2)}`} +
+
+
+
+ ); + }) + )} +
+
+ + {/* Character Detail Modal */} + +
+
+ handleCloseModal("action_detail_modal")} + > + ✕ + +
+ +
+

+ {transI18n("turnDetail").toUpperCase()} +

+
+ + {selectAvatar && selectTurn && ( + +
+

{transI18n("characterInformation")}

+
+
+

+ {transI18n("id")}: + {selectAvatar.id} +

+

+ {transI18n("character")}: + {getNameChar(locale, selectAvatar)} +

+
+
+ {getNameChar(locale, +
+
+
+ +
+
+

{transI18n("skillType")}

+

{transI18n(selectTurn?.skillType.toLowerCase())}

+
+
+

{transI18n("skillName")}

+

{selectTurn?.skillName}

+
+
+ +
+
+

{transI18n("actionValue")}

+

{selectTurn?.actionValue.toFixed(2)}

+
+
+

{transI18n("totalDamage")}

+

{selectTurn?.totalDamage.toFixed(2)}

+
+
+ +
+

{transI18n("damageDetails")}

+ {selectTurn?.damageDetail && selectTurn.damageDetail.length > 0 ? ( +
+ {selectTurn.damageDetail.map((detail, idx) => ( +

{detail.toFixed(2)}

+ ))} +
+ ) : ( +

{transI18n("noDamageDetail")}

+ )} +
+
+ )} + +
+ +
+
+ ); +} \ 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}${furigana}`; + }); +} + + +export default function CharacterCard({ data }: CharacterCardProps) { + const { locale } = useLocaleStore(); + const text = getNameChar(locale, data) + return ( +
  • +
    + +
    + ALT + {data.damageType.toLowerCase()} + {data.baseType.toLowerCase()} +
    +
    + + {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 */} +
    +
      +
    • + <> + { + const file = e.target.files?.[0]; + if (file) { + importBattleData(file, loadBattleDataFromJSON) + .then(() => console.log('Data loaded')) + .catch(err => alert('Failed to load data: ' + err.message)); + } + }} + /> + + +
    • +
    • + +
    • +
    • + +
    • +
    +
    + + + {/* Right side items */} +
    +
    +
    +
    + {status ? transI18n("connected") : transI18n("unconnected")} +
    +
    +
    +
    + {/* Language selector - REFINED */} +
    +
    + + + + + +
    +
    + +
    +
    + + + + + + +
    + +
    +
      + {themes.map((theme) => ( +
    • { + if (changeTheme) changeTheme(theme.label.toLowerCase()); + }} + > + +
    • + ))} +
    +
    +
    + + {/* GitHub Link */} + + + + + +
    + + +
    +
    + handleCloseModal("socket_connection_modal")} + > + ✕ + +
    + +
    +

    + {transI18n("socketConnection").toUpperCase()} +

    +
    + +
    + {/* Select connection type */} +
    + + +
    + + {/* Show host/port if Other */} + {connectionType === "Other" && ( +
    +
    + + setHost(e.target.value)} + /> +
    + +
    + + setPort(parseInt(e.target.value) || 0)} + /> +
    +
    + )} + + {message.text && ( +
    + {message.text} +
    + )} + +
    +
    + {transI18n("status")}: + + {status ? transI18n("connected") : transI18n("unconnected")} + +
    + +
    + + +
    +
    +
    +
    +
    +
    + ) +} \ 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 */} + +
    +
    + handleCloseModal("character_detail_modal")} + > + ✕ + +
    + +
    +

    + {selectedCharacter ? getNameChar(locale, selectedCharacter).toUpperCase() : ""} +

    +
    + + {selectedCharacter && ( + +
    +

    {transI18n("characterInformation")}

    +
    +
    +
    +

    + {transI18n("id")}: {selectedCharacter.id} +

    +

    + {transI18n("path")}: + {transI18n(selectedCharacter.baseType.toLowerCase())} + {selectedCharacter.baseType && ( + {selectedCharacter.baseType.toLowerCase()} + )} +

    +

    + {transI18n("rarity")}: {selectedCharacter.rank === "CombatPowerAvatarRarityType5" ? "5*" : "4*"} +

    +

    + {transI18n("element")}: + {transI18n(selectedCharacter.damageType.toLowerCase())} + {selectedCharacter.damageType && ( + {selectedCharacter.damageType.toLowerCase()} + )} +

    +
    + +
    + +
    + {selectedCharacter && ( + {getNameChar(locale, + )} +
    +
    +
    +
    +

    {transI18n("totalTurn")}: {totalTurn.toFixed(2)}

    +
    +
    +

    {transI18n("totalDamage")}: {totalDamage.toFixed(2)}

    +
    + + +
    +

    {transI18n("skillDamageBreakdown")}

    + +
    + +
    +
    +

    {transI18n("damageOverTime")}

    +
    + {[0, 1].map((m) => ( + + ))} +
    +
    + + +
    + +
    +

    {transI18n("skillUsageDistribution")}

    + +
    + +
    +
    +

    {transI18n("damagerPerCycle")}

    +
    + {[0, 1, 2].map((m) => ( + + ))} +
    +
    + +
    + +
    + )} +
    +
    +
    +
    + ); +} \ 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