Refactor TypeScript interfaces and types across multiple files for consistency and clarity
Gitea Auto Deploy / Deploy-Container (push) Failing after 46s
Gitea Auto Deploy / Deploy-Container (push) Failing after 46s
- Updated metadata interfaces in `metaData.ts` for improved readability. - Refactored `mics.ts` types to enhance structure and maintainability. - Added `Invasion` type to `mocDetail.ts` and `peakDetail.ts` for new functionality. - Simplified modal configuration in `modelConfig.ts`. - Cleaned up monster detail interfaces in `monsterDetail.ts`. - Enhanced battle configuration types in `pfDetail.ts` to include `Invasion`. - Updated JSON schemas in `srtools.zod.ts` to use `nullish` for optional fields. - Introduced new skills-lock.json for skill management. - Added global type declarations for CSS and SCSS modules in `global.d.ts`.
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
|
.agents/
|
||||||
.history/
|
.history/
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
+13
-5
@@ -1,18 +1,26 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"version": "4.4.0",
|
"version": "4.4.5",
|
||||||
"date": "15/07/2026",
|
"date": "16/07/2026",
|
||||||
"type": "update",
|
"type": "update",
|
||||||
"items": [
|
"items": [
|
||||||
"New data for 4.4.0"
|
"New data for 4.4.5"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"version": "4.3.0",
|
"version": "4.3.5",
|
||||||
|
"date": "02/06/2026",
|
||||||
|
"type": "update",
|
||||||
|
"items": [
|
||||||
|
"New data for 4.3.5"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "4.2.5",
|
||||||
"date": "01/05/2026",
|
"date": "01/05/2026",
|
||||||
"type": "update",
|
"type": "update",
|
||||||
"items": [
|
"items": [
|
||||||
"New data for 4.3.0"
|
"New data for 4.2.5"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -6,6 +6,7 @@ const eslintConfig = [...nextCoreWebVitals, ...nextTypescript, {
|
|||||||
"node_modules/**",
|
"node_modules/**",
|
||||||
".next/**",
|
".next/**",
|
||||||
".history/**",
|
".history/**",
|
||||||
|
".agents/**",
|
||||||
"out/**",
|
"out/**",
|
||||||
"build/**",
|
"build/**",
|
||||||
"next-env.d.ts",
|
"next-env.d.ts",
|
||||||
|
|||||||
+4
-1
@@ -228,6 +228,7 @@
|
|||||||
"bpskill": "Fähigkeit",
|
"bpskill": "Fähigkeit",
|
||||||
"maze": "Technik",
|
"maze": "Technik",
|
||||||
"ultra": "Ultimate",
|
"ultra": "Ultimate",
|
||||||
|
"assist": "Assist-Fähigkeit",
|
||||||
"servantskill": "Memosprite-Fähigkeit",
|
"servantskill": "Memosprite-Fähigkeit",
|
||||||
"severaltalent": "Memosprite-Talent",
|
"severaltalent": "Memosprite-Talent",
|
||||||
"singleattack": "Einzelangriff",
|
"singleattack": "Einzelangriff",
|
||||||
@@ -285,6 +286,8 @@
|
|||||||
"trailblazer": "Trailblazer",
|
"trailblazer": "Trailblazer",
|
||||||
"listExtraEffect": "Liste Zusatzeffekte",
|
"listExtraEffect": "Liste Zusatzeffekte",
|
||||||
"extra": "Extra",
|
"extra": "Extra",
|
||||||
"customLineup": "Benutzerdefinierte Aufstellung"
|
"customLineup": "Benutzerdefinierte Aufstellung",
|
||||||
|
"def": "VTD",
|
||||||
|
"language": "Sprache"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-1
@@ -37,6 +37,7 @@
|
|||||||
"wind": "Wind",
|
"wind": "Wind",
|
||||||
"hp": "Hp",
|
"hp": "Hp",
|
||||||
"atk": "Atk",
|
"atk": "Atk",
|
||||||
|
"def": "Def",
|
||||||
"speed": "Speed",
|
"speed": "Speed",
|
||||||
"critRate": "Crit Rate",
|
"critRate": "Crit Rate",
|
||||||
"critDmg": "Crit Dmg",
|
"critDmg": "Crit Dmg",
|
||||||
@@ -228,6 +229,7 @@
|
|||||||
"bpskill": "Skill",
|
"bpskill": "Skill",
|
||||||
"maze": "Technique",
|
"maze": "Technique",
|
||||||
"ultra": "Ultimate",
|
"ultra": "Ultimate",
|
||||||
|
"assist": "Assist Skill",
|
||||||
"servantskill": "Memosprite Skill",
|
"servantskill": "Memosprite Skill",
|
||||||
"severaltalent": "Memosprite Talent",
|
"severaltalent": "Memosprite Talent",
|
||||||
"singleattack": "Single Attack",
|
"singleattack": "Single Attack",
|
||||||
@@ -285,6 +287,7 @@
|
|||||||
"trailblazer": "Trailblazer",
|
"trailblazer": "Trailblazer",
|
||||||
"listExtraEffect": "List Extra Effect",
|
"listExtraEffect": "List Extra Effect",
|
||||||
"extra": "Extra",
|
"extra": "Extra",
|
||||||
"customLineup": "Custom Lineup"
|
"customLineup": "Custom Lineup",
|
||||||
|
"language": "Language"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-1
@@ -228,6 +228,7 @@
|
|||||||
"bpskill": "Habilidad",
|
"bpskill": "Habilidad",
|
||||||
"maze": "Técnica",
|
"maze": "Técnica",
|
||||||
"ultra": "Habilidad Definitiva",
|
"ultra": "Habilidad Definitiva",
|
||||||
|
"assist": "Habilidad de asistencia",
|
||||||
"servantskill": "Habilidad de Mnemoduende",
|
"servantskill": "Habilidad de Mnemoduende",
|
||||||
"severaltalent": "Talento de Mnemoduende",
|
"severaltalent": "Talento de Mnemoduende",
|
||||||
"singleattack": "Ataque Individual",
|
"singleattack": "Ataque Individual",
|
||||||
@@ -285,6 +286,8 @@
|
|||||||
"trailblazer": "Trazacaminos",
|
"trailblazer": "Trazacaminos",
|
||||||
"listExtraEffect": "Lista de Efectos Extra",
|
"listExtraEffect": "Lista de Efectos Extra",
|
||||||
"extra": "Extra",
|
"extra": "Extra",
|
||||||
"customLineup": "Alineación de equipo personalizada"
|
"customLineup": "Alineación de equipo personalizada",
|
||||||
|
"def": "DEF",
|
||||||
|
"language": "Idioma"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-1
@@ -228,6 +228,7 @@
|
|||||||
"bpskill": "Compétence",
|
"bpskill": "Compétence",
|
||||||
"maze": "Technique",
|
"maze": "Technique",
|
||||||
"ultra": "Ultime",
|
"ultra": "Ultime",
|
||||||
|
"assist": "Compétence d'assistance",
|
||||||
"servantskill": "Compétence de Mémolutin",
|
"servantskill": "Compétence de Mémolutin",
|
||||||
"severaltalent": "Talent de Mémolutin",
|
"severaltalent": "Talent de Mémolutin",
|
||||||
"singleattack": "Attaque à cible unique",
|
"singleattack": "Attaque à cible unique",
|
||||||
@@ -285,6 +286,8 @@
|
|||||||
"trailblazer": "Pionnier",
|
"trailblazer": "Pionnier",
|
||||||
"listExtraEffect": "Liste des effets supplémentaires",
|
"listExtraEffect": "Liste des effets supplémentaires",
|
||||||
"extra": "Extra",
|
"extra": "Extra",
|
||||||
"customLineup": "Composition personnalisée"
|
"customLineup": "Composition personnalisée",
|
||||||
|
"def": "DÉF",
|
||||||
|
"language": "Langue"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-1
@@ -228,6 +228,7 @@
|
|||||||
"bpskill": "Skill",
|
"bpskill": "Skill",
|
||||||
"maze": "Technique",
|
"maze": "Technique",
|
||||||
"ultra": "Ultimate",
|
"ultra": "Ultimate",
|
||||||
|
"assist": "Skill Bantuan",
|
||||||
"servantskill": "Skill Memosprite",
|
"servantskill": "Skill Memosprite",
|
||||||
"severaltalent": "Talent Memosprite",
|
"severaltalent": "Talent Memosprite",
|
||||||
"singleattack": "Single Attack",
|
"singleattack": "Single Attack",
|
||||||
@@ -285,6 +286,8 @@
|
|||||||
"trailblazer": "Trailblazer",
|
"trailblazer": "Trailblazer",
|
||||||
"listExtraEffect": "Daftar Efek Ekstra",
|
"listExtraEffect": "Daftar Efek Ekstra",
|
||||||
"extra": "Ekstra",
|
"extra": "Ekstra",
|
||||||
"customLineup": "Lineup Kustom"
|
"customLineup": "Lineup Kustom",
|
||||||
|
"def": "DEF",
|
||||||
|
"language": "Bahasa"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-1
@@ -225,6 +225,7 @@
|
|||||||
"details": "詳細",
|
"details": "詳細",
|
||||||
"normal": "通常攻撃",
|
"normal": "通常攻撃",
|
||||||
"bpskill": "戦闘スキル",
|
"bpskill": "戦闘スキル",
|
||||||
|
"assist": "アシストスキル",
|
||||||
"maze": "秘技",
|
"maze": "秘技",
|
||||||
"ultra": "必殺技",
|
"ultra": "必殺技",
|
||||||
"servantskill": "メモスプライトスキル",
|
"servantskill": "メモスプライトスキル",
|
||||||
@@ -284,6 +285,8 @@
|
|||||||
"trailblazer": "開拓者",
|
"trailblazer": "開拓者",
|
||||||
"listExtraEffect": "追加効果一覧",
|
"listExtraEffect": "追加効果一覧",
|
||||||
"extra": "追加",
|
"extra": "追加",
|
||||||
"customLineup": "カスタム編成"
|
"customLineup": "カスタム編成",
|
||||||
|
"def": "防御力",
|
||||||
|
"language": "言語"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-1
@@ -225,6 +225,7 @@
|
|||||||
"details": "상세",
|
"details": "상세",
|
||||||
"normal": "기본 공격",
|
"normal": "기본 공격",
|
||||||
"bpskill": "스킬",
|
"bpskill": "스킬",
|
||||||
|
"assist": "지원 스킬",
|
||||||
"maze": "전술",
|
"maze": "전술",
|
||||||
"ultra": "필살기",
|
"ultra": "필살기",
|
||||||
"servantskill": "메모스프라이트 스킬",
|
"servantskill": "메모스프라이트 스킬",
|
||||||
@@ -284,6 +285,8 @@
|
|||||||
"trailblazer": "개척자",
|
"trailblazer": "개척자",
|
||||||
"listExtraEffect": "추가 효과 목록",
|
"listExtraEffect": "추가 효과 목록",
|
||||||
"extra": "추가",
|
"extra": "추가",
|
||||||
"customLineup": "커스텀 편성"
|
"customLineup": "커스텀 편성",
|
||||||
|
"def": "방어력",
|
||||||
|
"language": "언어"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-1
@@ -228,6 +228,7 @@
|
|||||||
"bpskill": "Perícia",
|
"bpskill": "Perícia",
|
||||||
"maze": "Técnica",
|
"maze": "Técnica",
|
||||||
"ultra": "Perícia Suprema",
|
"ultra": "Perícia Suprema",
|
||||||
|
"assist": "Habilidade de Assistência",
|
||||||
"servantskill": "Habilidade do Memoespirito",
|
"servantskill": "Habilidade do Memoespirito",
|
||||||
"severaltalent": "Talento do Memoespirito",
|
"severaltalent": "Talento do Memoespirito",
|
||||||
"singleattack": "Ataque Único",
|
"singleattack": "Ataque Único",
|
||||||
@@ -285,6 +286,8 @@
|
|||||||
"trailblazer": "Desbravador",
|
"trailblazer": "Desbravador",
|
||||||
"listExtraEffect": "Lista de Efeitos Extras",
|
"listExtraEffect": "Lista de Efeitos Extras",
|
||||||
"extra": "Extra",
|
"extra": "Extra",
|
||||||
"customLineup": "Formação Personalizada"
|
"customLineup": "Formação Personalizada",
|
||||||
|
"def": "DEF",
|
||||||
|
"language": "Idioma"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-1
@@ -228,6 +228,7 @@
|
|||||||
"bpskill": "Навык",
|
"bpskill": "Навык",
|
||||||
"maze": "Техника",
|
"maze": "Техника",
|
||||||
"ultra": "Сверхспособность",
|
"ultra": "Сверхспособность",
|
||||||
|
"assist": "Навык поддержки",
|
||||||
"servantskill": "Навык мемоспрайта",
|
"servantskill": "Навык мемоспрайта",
|
||||||
"severaltalent": "Талант мемоспрайта",
|
"severaltalent": "Талант мемоспрайта",
|
||||||
"singleattack": "Одиночная атака",
|
"singleattack": "Одиночная атака",
|
||||||
@@ -285,6 +286,8 @@
|
|||||||
"trailblazer": "Первопроходец",
|
"trailblazer": "Первопроходец",
|
||||||
"listExtraEffect": "Список доп. эффектов",
|
"listExtraEffect": "Список доп. эффектов",
|
||||||
"extra": "Экстра",
|
"extra": "Экстра",
|
||||||
"customLineup": "Пользовательский отряд"
|
"customLineup": "Пользовательский отряд",
|
||||||
|
"def": "Защ",
|
||||||
|
"language": "Язык"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-1
@@ -228,6 +228,7 @@
|
|||||||
"bpskill": "สกิลต่อสู้",
|
"bpskill": "สกิลต่อสู้",
|
||||||
"maze": "เทคนิค",
|
"maze": "เทคนิค",
|
||||||
"ultra": "ท่าไม้ตาย",
|
"ultra": "ท่าไม้ตาย",
|
||||||
|
"assist": "สกิลช่วยเหลือ",
|
||||||
"servantskill": "สกิล Memosprite",
|
"servantskill": "สกิล Memosprite",
|
||||||
"severaltalent": "พรสวรรค์ Memosprite",
|
"severaltalent": "พรสวรรค์ Memosprite",
|
||||||
"singleattack": "โจมตีเป้าหมายเดียว",
|
"singleattack": "โจมตีเป้าหมายเดียว",
|
||||||
@@ -285,6 +286,8 @@
|
|||||||
"trailblazer": "ผู้บุกเบิก",
|
"trailblazer": "ผู้บุกเบิก",
|
||||||
"listExtraEffect": "รายการเอฟเฟกต์พิเศษ",
|
"listExtraEffect": "รายการเอฟเฟกต์พิเศษ",
|
||||||
"extra": "พิเศษ",
|
"extra": "พิเศษ",
|
||||||
"customLineup": "จัดทีมแบบกำหนดเอง"
|
"customLineup": "จัดทีมแบบกำหนดเอง",
|
||||||
|
"def": "DEF",
|
||||||
|
"language": "ภาษา"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+5
-2
@@ -126,7 +126,7 @@
|
|||||||
"lightconeEquipment": "Trang bị nón ánh sáng",
|
"lightconeEquipment": "Trang bị nón ánh sáng",
|
||||||
"lightconeSettings": "Cài đặt nón ánh sáng",
|
"lightconeSettings": "Cài đặt nón ánh sáng",
|
||||||
"placeholderLevel": "Nhập cấp độ",
|
"placeholderLevel": "Nhập cấp độ",
|
||||||
"superimpositionRank": "Bậc chồng kỹ năng",
|
"superimpositionRank": "Tích tầng",
|
||||||
"ranksNote": "Bậc càng cao hiệu ứng càng mạnh",
|
"ranksNote": "Bậc càng cao hiệu ứng càng mạnh",
|
||||||
"changeLightcone": "Thay đổi nón ánh sáng",
|
"changeLightcone": "Thay đổi nón ánh sáng",
|
||||||
"removeLightcone": "Gỡ nón ánh sáng",
|
"removeLightcone": "Gỡ nón ánh sáng",
|
||||||
@@ -228,6 +228,7 @@
|
|||||||
"bpskill": "Chiến kỹ",
|
"bpskill": "Chiến kỹ",
|
||||||
"maze": "Bí kỹ",
|
"maze": "Bí kỹ",
|
||||||
"ultra": "Tuyệt kỹ",
|
"ultra": "Tuyệt kỹ",
|
||||||
|
"assist": "Kỹ năng hỗ trợ",
|
||||||
"servantskill": "Kỹ năng vật triệu hồi",
|
"servantskill": "Kỹ năng vật triệu hồi",
|
||||||
"severaltalent": "Thiên phú vật triệu hồi",
|
"severaltalent": "Thiên phú vật triệu hồi",
|
||||||
"singleattack": "Tấn công đơn",
|
"singleattack": "Tấn công đơn",
|
||||||
@@ -284,6 +285,8 @@
|
|||||||
"trailblazer": "Nhà khai phá",
|
"trailblazer": "Nhà khai phá",
|
||||||
"listExtraEffect": "Danh sách hiệu ứng bổ sung",
|
"listExtraEffect": "Danh sách hiệu ứng bổ sung",
|
||||||
"extra": "Bổ sung",
|
"extra": "Bổ sung",
|
||||||
"customLineup": "Đội hình tùy chỉnh"
|
"customLineup": "Đội hình tùy chỉnh",
|
||||||
|
"def": "Phòng thủ",
|
||||||
|
"language": "Ngôn ngữ"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
-1
@@ -225,6 +225,7 @@
|
|||||||
"details": "详情",
|
"details": "详情",
|
||||||
"normal": "普通攻击",
|
"normal": "普通攻击",
|
||||||
"bpskill": "技能",
|
"bpskill": "技能",
|
||||||
|
"assist": "协战技",
|
||||||
"maze": "秘技",
|
"maze": "秘技",
|
||||||
"ultra": "终结技",
|
"ultra": "终结技",
|
||||||
"servantskill": "忆灵技能",
|
"servantskill": "忆灵技能",
|
||||||
@@ -284,6 +285,8 @@
|
|||||||
"trailblazer": "开拓者",
|
"trailblazer": "开拓者",
|
||||||
"listExtraEffect": "额外效果列表",
|
"listExtraEffect": "额外效果列表",
|
||||||
"extra": "额外",
|
"extra": "额外",
|
||||||
"customLineup": "自定义阵容"
|
"customLineup": "自定义阵容",
|
||||||
|
"def": "防御力",
|
||||||
|
"language": "语言"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+12
-12
@@ -10,15 +10,15 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/bundle-analyzer": "16.1.6",
|
"@next/bundle-analyzer": "16.1.6",
|
||||||
"@tanstack/react-query": "^5.100.10",
|
"@tanstack/react-query": "^5.102.5",
|
||||||
"axios": "^1.16.1",
|
"axios": "^1.20.0",
|
||||||
"fast-average-color": "^9.5.2",
|
"fast-average-color": "^9.5.2",
|
||||||
"framer-motion": "^12.38.0",
|
"framer-motion": "^12.43.0",
|
||||||
"html2canvas-pro": "^1.6.7",
|
"html2canvas-pro": "^1.6.7",
|
||||||
"lru-cache": "^11.3.6",
|
"lru-cache": "^11.5.2",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"next-intl": "^4.12.0",
|
"next-intl": "^4.13.7",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
"react-dom": "19.2.4",
|
"react-dom": "19.2.4",
|
||||||
@@ -27,20 +27,20 @@
|
|||||||
"react-toastify": "^11.1.0",
|
"react-toastify": "^11.1.0",
|
||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"zod": "^4.4.3",
|
"zod": "^4.4.3",
|
||||||
"zustand": "^5.0.13"
|
"zustand": "^5.0.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.3.0",
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^25.8.0",
|
"@types/node": "^25.9.5",
|
||||||
"@types/react": "19.2.6",
|
"@types/react": "19.2.6",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"baseline-browser-mapping": "^2.10.29",
|
"baseline-browser-mapping": "^2.11.19",
|
||||||
"daisyui": "^5.5.19",
|
"daisyui": "^5.7.22",
|
||||||
"eslint": "^9.39.4",
|
"eslint": "^9.39.5",
|
||||||
"eslint-config-next": "16.1.6",
|
"eslint-config-next": "16.1.6",
|
||||||
"tailwind-scrollbar": "^4.0.2",
|
"tailwind-scrollbar": "^4.0.2",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.3",
|
||||||
"ts-to-zod": "^5.1.0",
|
"ts-to-zod": "^5.1.0",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"skills": {
|
||||||
|
"banner-design": {
|
||||||
|
"source": "nextlevelbuilder/ui-ux-pro-max-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": ".claude/skills/banner-design/SKILL.md",
|
||||||
|
"computedHash": "f859a951ded1109a38bcc7f18faf4581f9adbf0550df7a11c013e77a5b229541"
|
||||||
|
},
|
||||||
|
"brand": {
|
||||||
|
"source": "nextlevelbuilder/ui-ux-pro-max-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": ".claude/skills/brand/SKILL.md",
|
||||||
|
"computedHash": "0e773774d46ecea78eff2b36f574e41830417fcc618181215611f336930deac2"
|
||||||
|
},
|
||||||
|
"brandkit": {
|
||||||
|
"source": "Leonxlnx/taste-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/brandkit/SKILL.md",
|
||||||
|
"computedHash": "735007879f5110aeae1e981a53fe74a12afde70347f9773dacd50e4697a091bf"
|
||||||
|
},
|
||||||
|
"design": {
|
||||||
|
"source": "nextlevelbuilder/ui-ux-pro-max-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": ".claude/skills/design/SKILL.md",
|
||||||
|
"computedHash": "bb2e3ffee4b7b69e5a38ce08f6010ca79381d9f53b9dfadb33aee1ece6f1679a"
|
||||||
|
},
|
||||||
|
"design-system": {
|
||||||
|
"source": "nextlevelbuilder/ui-ux-pro-max-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": ".claude/skills/design-system/SKILL.md",
|
||||||
|
"computedHash": "5a3dbae4a3c2ad9f96323d9bfae926b3c21bb1e3b066aa067906f577a2ff2545"
|
||||||
|
},
|
||||||
|
"design-taste-frontend": {
|
||||||
|
"source": "Leonxlnx/taste-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/taste-skill/SKILL.md",
|
||||||
|
"computedHash": "899b84384f74f540ea5284d9b2e9234e050998b42eacc805410b518d4226c0b3"
|
||||||
|
},
|
||||||
|
"design-taste-frontend-v1": {
|
||||||
|
"source": "Leonxlnx/taste-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/taste-skill-v1/SKILL.md",
|
||||||
|
"computedHash": "0a226c13d69639553cf8a7c40aa5527b026820f8ddaf7a6b263988e32ab44a27"
|
||||||
|
},
|
||||||
|
"full-output-enforcement": {
|
||||||
|
"source": "Leonxlnx/taste-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/output-skill/SKILL.md",
|
||||||
|
"computedHash": "524f7c7950c3fde726101613f630140411c7d080e63be718687f4b38f919be81"
|
||||||
|
},
|
||||||
|
"gpt-taste": {
|
||||||
|
"source": "Leonxlnx/taste-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/gpt-tasteskill/SKILL.md",
|
||||||
|
"computedHash": "7bd1bfdda0e51016ca222693401a65fc0dd9bf9487e0bd5b1740ba510f6f963a"
|
||||||
|
},
|
||||||
|
"high-end-visual-design": {
|
||||||
|
"source": "Leonxlnx/taste-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/soft-skill/SKILL.md",
|
||||||
|
"computedHash": "f730e4132775f13eea19e3dc39afc6bb453cfc0498872b127ad8f0d47cfd802d"
|
||||||
|
},
|
||||||
|
"image-to-code": {
|
||||||
|
"source": "Leonxlnx/taste-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/image-to-code-skill/SKILL.md",
|
||||||
|
"computedHash": "3d2ac14d956fdafc5500d2108b90895e134d847f9f199e7f1d18bf1f814cf7c8"
|
||||||
|
},
|
||||||
|
"imagegen-frontend-mobile": {
|
||||||
|
"source": "Leonxlnx/taste-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/imagegen-frontend-mobile/SKILL.md",
|
||||||
|
"computedHash": "7c1d1dc59cd0df4063b8621af543bf5dea1d576b261507e17b0323c69680b048"
|
||||||
|
},
|
||||||
|
"imagegen-frontend-web": {
|
||||||
|
"source": "Leonxlnx/taste-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/imagegen-frontend-web/SKILL.md",
|
||||||
|
"computedHash": "27255c7c50181e58add156b2aa2bd5325285b85f04027e4c8b73742d98d7be0a"
|
||||||
|
},
|
||||||
|
"industrial-brutalist-ui": {
|
||||||
|
"source": "Leonxlnx/taste-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/brutalist-skill/SKILL.md",
|
||||||
|
"computedHash": "e66e5a2a8b73cef79ab6546459c665e8b983645689c5f3772f7eed8b9f1e8e0e"
|
||||||
|
},
|
||||||
|
"minimalist-ui": {
|
||||||
|
"source": "Leonxlnx/taste-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/minimalist-skill/SKILL.md",
|
||||||
|
"computedHash": "7e88897e0371184e97a82b8d9ef080323c13f21eb067b58ef71eb05173a08ac2"
|
||||||
|
},
|
||||||
|
"redesign-existing-projects": {
|
||||||
|
"source": "Leonxlnx/taste-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/redesign-skill/SKILL.md",
|
||||||
|
"computedHash": "47eced3e960a2c961d3cbee11fa1216264f589344b586a5cbd03967cd6b9c54b"
|
||||||
|
},
|
||||||
|
"slides": {
|
||||||
|
"source": "nextlevelbuilder/ui-ux-pro-max-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": ".claude/skills/slides/SKILL.md",
|
||||||
|
"computedHash": "a2b3559b58afc484548e1eb359e20495d11cf12b90da7c1fbba50efed9a70340"
|
||||||
|
},
|
||||||
|
"stitch-design-taste": {
|
||||||
|
"source": "Leonxlnx/taste-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/stitch-skill/SKILL.md",
|
||||||
|
"computedHash": "c96b580192b34167efa01f460bbc669ff4315cf97f3cf6ff85ded8029aa720b8"
|
||||||
|
},
|
||||||
|
"ui-styling": {
|
||||||
|
"source": "nextlevelbuilder/ui-ux-pro-max-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": ".claude/skills/ui-styling/SKILL.md",
|
||||||
|
"computedHash": "e10b225db04fdfd893fdda2b9ed893c315f9dee5693494743d57b02066e96a51"
|
||||||
|
},
|
||||||
|
"ui-ux-pro-max": {
|
||||||
|
"source": "nextlevelbuilder/ui-ux-pro-max-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": ".claude/skills/ui-ux-pro-max/SKILL.md",
|
||||||
|
"computedHash": "d1de8ff8d228cd4204bf06eb959ef4317d5b927908d9f42765971e33e80c8a7e"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+79
-7
@@ -1,6 +1,5 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
|
||||||
@plugin "daisyui" {
|
@plugin "daisyui" {
|
||||||
exclude: properties;
|
exclude: properties;
|
||||||
themes: winter --default, night --prefersdark, cupcake, coffee;
|
themes: winter --default, night --prefersdark, cupcake, coffee;
|
||||||
@@ -9,19 +8,19 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@plugin 'tailwind-scrollbar' {
|
@plugin 'tailwind-scrollbar' {
|
||||||
nocompatible: true;
|
nocompatible: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--size-big: 4vw;
|
--size-big: 4vw;
|
||||||
--size-medium: 3vw;
|
--size-medium: 3vw;
|
||||||
--size-small: 2vw;
|
--size-small: 2vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Smooth Modern Ultra-Sleek Scrollbars */
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 4px;
|
||||||
height: 6px;
|
height: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
@@ -29,10 +28,83 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: #9ca3af;
|
background: rgba(156, 163, 175, 0.28);
|
||||||
border-radius: 0;
|
border-radius: 9999px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(156, 163, 175, 0.55);
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-button {
|
::-webkit-scrollbar-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Custom UI Utilities for Gaming Companion Aesthetic */
|
||||||
|
@layer utilities {
|
||||||
|
.glass-card {
|
||||||
|
background-color: var(--color-base-100);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--color-base-content) 10%, transparent);
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glass-panel {
|
||||||
|
background-color: var(--color-base-100);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--color-base-content) 10%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover-lift {
|
||||||
|
transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.25s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.25s ease;
|
||||||
|
}
|
||||||
|
.hover-lift:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-press {
|
||||||
|
transition: transform 0.15s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
}
|
||||||
|
.active-press:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Rarity Glow Effects */
|
||||||
|
.glow-5star {
|
||||||
|
box-shadow: 0 0 16px -2px rgba(245, 158, 11, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.glow-4star {
|
||||||
|
box-shadow: 0 0 16px -2px rgba(168, 85, 247, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ambient Background - Clean Canvas */
|
||||||
|
.bg-ambient-mesh {
|
||||||
|
background-color: var(--color-base-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme Adaptive Path & Stat Icon Filters - High Accessibility Contrast */
|
||||||
|
.theme-path-icon {
|
||||||
|
filter: brightness(0.2) opacity(0.85);
|
||||||
|
}
|
||||||
|
[data-theme="night"] .theme-path-icon,
|
||||||
|
[data-theme="coffee"] .theme-path-icon {
|
||||||
|
filter: brightness(0.8) opacity(0.85);
|
||||||
|
}
|
||||||
|
.theme-path-icon-active {
|
||||||
|
filter: brightness(0);
|
||||||
|
}
|
||||||
|
[data-theme="night"] .theme-path-icon-active,
|
||||||
|
[data-theme="coffee"] .theme-path-icon-active {
|
||||||
|
filter: brightness(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stat icons (HSR raw sprite icons are white pngs, need dark filter on light themes) */
|
||||||
|
.theme-stat-icon {
|
||||||
|
filter: brightness(0.15) opacity(0.8);
|
||||||
|
}
|
||||||
|
[data-theme="night"] .theme-stat-icon,
|
||||||
|
[data-theme="coffee"] .theme-stat-icon {
|
||||||
|
filter: brightness(0.95) opacity(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-10
@@ -64,9 +64,9 @@ export default async function RootLayout({
|
|||||||
const locale = await getLocale()
|
const locale = await getLocale()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale} suppressHydrationWarning>
|
<html lang={locale} suppressHydrationWarning className="overflow-x-hidden">
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased overflow-x-hidden max-w-full`}
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
>
|
>
|
||||||
<NextIntlClientProvider messages={messages}>
|
<NextIntlClientProvider messages={messages}>
|
||||||
@@ -74,17 +74,19 @@ export default async function RootLayout({
|
|||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<ClientThemeWrapper>
|
<ClientThemeWrapper>
|
||||||
<ClientDataFetcher >
|
<ClientDataFetcher >
|
||||||
<div className="min-h-screen w-full">
|
<div className="min-h-screen w-full flex flex-col justify-between bg-ambient-mesh selection:bg-primary/20 selection:text-primary">
|
||||||
<Header />
|
<Header />
|
||||||
<div className="grid grid-cols-12 w-full">
|
<main className="grid grid-cols-12 w-full flex-1">
|
||||||
<div className="hidden sm:block md:col-span-4 lg:col-span-3 sticky top-0 self-start h-fit">
|
<aside className="hidden sm:block sm:col-span-4 md:col-span-4 lg:col-span-3 sticky top-16 self-start h-[calc(100vh-4rem)] overflow-hidden border-r border-base-content/10 bg-base-100/30 backdrop-blur-sm">
|
||||||
<AvatarBar />
|
<AvatarBar />
|
||||||
</div>
|
</aside>
|
||||||
<div className="col-span-12 sm:col-span-8 lg:col-span-9">
|
<section className="col-span-12 sm:col-span-8 md:col-span-8 lg:col-span-9 flex flex-col min-w-0">
|
||||||
<ActionBar />
|
<ActionBar />
|
||||||
{children}
|
<div className="flex-1 p-2 sm:p-3 sm:pt-1">
|
||||||
</div>
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+302
-237
@@ -3,15 +3,14 @@
|
|||||||
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from 'next/navigation'
|
import { usePathname, useRouter } from 'next/navigation'
|
||||||
import ParseText from "../parseText";
|
import ParseText from "../parseText";
|
||||||
import useLocaleStore from "@/stores/localeStore";
|
import useLocaleStore from "@/stores/localeStore";
|
||||||
import { getNameChar } from "@/helper/getName";
|
import { getNameChar } from "@/helper/getName";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import useModelStore from "@/stores/modelStore";
|
import useModelStore from "@/stores/modelStore";
|
||||||
import useUserDataStore from "@/stores/userDataStore";
|
import useUserDataStore from "@/stores/userDataStore";
|
||||||
import { ModalConfig, RelicStore } from "@/types";
|
import { ModalConfig, RelicStore, AvatarProfileStore } from "@/types";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import useGlobalStore from "@/stores/globalStore";
|
import useGlobalStore from "@/stores/globalStore";
|
||||||
import { connectToPS, syncDataToPS } from "@/helper";
|
import { connectToPS, syncDataToPS } from "@/helper";
|
||||||
@@ -20,14 +19,28 @@ import useCopyProfileStore from "@/stores/copyProfile";
|
|||||||
import AvatarBar from "../avatarBar";
|
import AvatarBar from "../avatarBar";
|
||||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||||
import useDetailDataStore from "@/stores/detailDataStore";
|
import useDetailDataStore from "@/stores/detailDataStore";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
import { User, Shield, Sparkles, Zap, Image as ImageIcon, RefreshCw, Plus, Copy, Edit2, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
export default function ActionBar() {
|
export default function ActionBar() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { avatarSelected } = useCurrentDataStore()
|
const pathname = usePathname()
|
||||||
const { damageType, baseType } = useDetailDataStore()
|
const { avatarSelected } = useCurrentDataStore(
|
||||||
const { setResetData } = useCopyProfileStore()
|
useShallow((state) => ({ avatarSelected: state.avatarSelected }))
|
||||||
|
)
|
||||||
|
const { damageType, baseType } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
damageType: state.damageType,
|
||||||
|
baseType: state.baseType,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { setResetData } = useCopyProfileStore(
|
||||||
|
useShallow((state) => ({ setResetData: state.setResetData }))
|
||||||
|
)
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
const { locale } = useLocaleStore()
|
const { locale } = useLocaleStore(
|
||||||
|
useShallow((state) => ({ locale: state.locale }))
|
||||||
|
)
|
||||||
const {
|
const {
|
||||||
isOpenCreateProfile,
|
isOpenCreateProfile,
|
||||||
setIsOpenCreateProfile,
|
setIsOpenCreateProfile,
|
||||||
@@ -35,17 +48,33 @@ export default function ActionBar() {
|
|||||||
setIsOpenCopy,
|
setIsOpenCopy,
|
||||||
isOpenAvatars,
|
isOpenAvatars,
|
||||||
setIsOpenAvatars
|
setIsOpenAvatars
|
||||||
} = useModelStore()
|
} = useModelStore(
|
||||||
const { avatars, setAvatar } = useUserDataStore()
|
useShallow((state) => ({
|
||||||
|
isOpenCreateProfile: state.isOpenCreateProfile,
|
||||||
|
setIsOpenCreateProfile: state.setIsOpenCreateProfile,
|
||||||
|
isOpenCopy: state.isOpenCopy,
|
||||||
|
setIsOpenCopy: state.setIsOpenCopy,
|
||||||
|
isOpenAvatars: state.isOpenAvatars,
|
||||||
|
setIsOpenAvatars: state.setIsOpenAvatars,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { avatars, setAvatar } = useUserDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
avatars: state.avatars,
|
||||||
|
setAvatar: state.setAvatar,
|
||||||
|
}))
|
||||||
|
)
|
||||||
const [profileName, setProfileName] = useState("");
|
const [profileName, setProfileName] = useState("");
|
||||||
const [formState, setFormState] = useState("EDIT");
|
const [formState, setFormState] = useState("EDIT");
|
||||||
const [profileEdit, setProfileEdit] = useState(-1);
|
const [profileEdit, setProfileEdit] = useState(-1);
|
||||||
const { isConnectPS } = useGlobalStore()
|
const { isConnectPS } = useGlobalStore(
|
||||||
|
useShallow((state) => ({ isConnectPS: state.isConnectPS }))
|
||||||
|
)
|
||||||
|
|
||||||
const profileCurrent = useMemo(() => {
|
const profileCurrent = useMemo(() => {
|
||||||
if (!avatarSelected) return null;
|
if (!avatarSelected) return null;
|
||||||
const avatar = avatars[avatarSelected.ID];
|
const avatar = avatars[avatarSelected.ID];
|
||||||
return avatar?.profileList[avatar.profileSelect] || null;
|
return avatar?.profileList?.[avatar?.profileSelect] ?? avatar?.profileList?.[0] ?? null;
|
||||||
}, [avatarSelected, avatars]);
|
}, [avatarSelected, avatars]);
|
||||||
|
|
||||||
const listProfile = useMemo(() => {
|
const listProfile = useMemo(() => {
|
||||||
@@ -54,29 +83,27 @@ export default function ActionBar() {
|
|||||||
return avatar?.profileList || [];
|
return avatar?.profileList || [];
|
||||||
}, [avatarSelected, avatars]);
|
}, [avatarSelected, avatars]);
|
||||||
|
|
||||||
|
|
||||||
const handleUpdateProfile = () => {
|
const handleUpdateProfile = () => {
|
||||||
if (!profileName.trim()) return;
|
if (!profileName.trim()) return;
|
||||||
if (formState === "CREATE" && avatarSelected && avatars[avatarSelected.ID]) {
|
if (formState === "CREATE" && avatarSelected && avatars[avatarSelected.ID]) {
|
||||||
const newListProfile = [...listProfile]
|
const newProfile: AvatarProfileStore = {
|
||||||
const newProfile = {
|
|
||||||
profile_name: profileName,
|
profile_name: profileName,
|
||||||
lightcone: null,
|
lightcone: null,
|
||||||
relics: {} as Record<string, RelicStore>
|
relics: {} as Record<string, RelicStore>
|
||||||
}
|
};
|
||||||
newListProfile.push(newProfile)
|
const newListProfile = [...listProfile, newProfile];
|
||||||
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: newListProfile.length - 1 })
|
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: newListProfile.length - 1 })
|
||||||
toast.success("Profile created successfully")
|
toast.success("Profile created successfully")
|
||||||
} else if (formState === "EDIT" && profileCurrent && avatarSelected && profileEdit !== -1) {
|
} else if (formState === "EDIT" && profileCurrent && avatarSelected && profileEdit !== -1) {
|
||||||
const newListProfile = [...listProfile]
|
const newListProfile = listProfile.map((p, idx) =>
|
||||||
newListProfile[profileEdit].profile_name = profileName;
|
idx === profileEdit ? { ...p, profile_name: profileName } : p
|
||||||
|
);
|
||||||
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile })
|
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile })
|
||||||
toast.success("Profile updated successfully")
|
toast.success("Profile updated successfully")
|
||||||
}
|
}
|
||||||
handleCloseModal("update_profile_modal");
|
handleCloseModal("update_profile_modal");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleShow = (modalId: string) => {
|
const handleShow = (modalId: string) => {
|
||||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||||
if (modal) {
|
if (modal) {
|
||||||
@@ -92,28 +119,32 @@ export default function ActionBar() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const actionMove = (path: string) => {
|
const actionMove = (path: string) => {
|
||||||
router.push(`/${path}`)
|
router.push(`/${path}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleProfileSelect = (profileId: number) => {
|
const handleProfileSelect = (profileId: number) => {
|
||||||
if (!avatarSelected) return;
|
if (!avatarSelected || !avatars[avatarSelected.ID]) return;
|
||||||
if (avatars[avatarSelected.ID].profileSelect === profileId) return;
|
if (avatars[avatarSelected.ID].profileSelect === profileId) return;
|
||||||
setAvatar({ ...avatars[avatarSelected.ID], profileSelect: profileId })
|
setAvatar({ ...avatars[avatarSelected.ID], profileSelect: profileId })
|
||||||
toast.success(`Profile changed to Profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}`)
|
const selectedProfile = avatars[avatarSelected.ID].profileList?.[profileId];
|
||||||
|
if (selectedProfile) {
|
||||||
|
toast.success(`Profile changed to: ${selectedProfile.profile_name}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteProfile = (profileId: number, e: React.MouseEvent) => {
|
const handleDeleteProfile = (profileId: number, e: React.MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
if (!avatarSelected || profileId == 0) return;
|
if (!avatarSelected || profileId === 0 || !avatars[avatarSelected.ID]) return;
|
||||||
if (window.confirm(`Are you sure you want to delete profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}?`)) {
|
const targetProfile = avatars[avatarSelected.ID].profileList?.[profileId];
|
||||||
const newListProfile = [...listProfile]
|
if (!targetProfile) return;
|
||||||
newListProfile.splice(profileId, 1)
|
if (window.confirm(`Are you sure you want to delete profile: ${targetProfile.profile_name}?`)) {
|
||||||
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: profileId - 1 })
|
const currentSelect = avatars[avatarSelected.ID].profileSelect;
|
||||||
toast.success(`Profile ${avatars[avatarSelected.ID].profileList[profileId].profile_name} deleted successfully`)
|
const newListProfile = listProfile.filter((_, idx) => idx !== profileId);
|
||||||
|
const nextSelect = currentSelect >= profileId ? Math.max(0, currentSelect - 1) : currentSelect;
|
||||||
|
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: nextSelect })
|
||||||
|
toast.success(`Profile ${targetProfile.profile_name} deleted successfully`)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleConnectOrSyncPS = async () => {
|
const handleConnectOrSyncPS = async () => {
|
||||||
@@ -144,23 +175,27 @@ export default function ActionBar() {
|
|||||||
handleCloseModal("update_profile_modal")
|
handleCloseModal("update_profile_modal")
|
||||||
},
|
},
|
||||||
content: (
|
content: (
|
||||||
<div className="px-6 space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="label-text text-primary font-semibold text-lg">
|
<span className="label-text font-semibold text-base-content/80">
|
||||||
{transI18n("profileName")}
|
{transI18n("profileName")}
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={transI18n("placeholderProfileName")}
|
placeholder={transI18n("placeholderProfileName")}
|
||||||
className="input input-warning mt-1 w-full"
|
className="input input-bordered input-primary w-full"
|
||||||
value={profileName}
|
value={profileName}
|
||||||
onChange={(e) => setProfileName(e.target.value)}
|
onChange={(e) => setProfileName(e.target.value)}
|
||||||
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="modal-action">
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
<button className="btn btn-success btn-sm sm:btn-md" onClick={handleUpdateProfile}>
|
<button className="btn btn-ghost" onClick={() => setIsOpenCreateProfile(false)}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-primary" onClick={handleUpdateProfile}>
|
||||||
{formState === "CREATE" ? transI18n("create") : transI18n("update")}
|
{formState === "CREATE" ? transI18n("create") : transI18n("update")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,7 +204,7 @@ export default function ActionBar() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "copy_profile_modal",
|
id: "copy_profile_modal",
|
||||||
title: transI18n("copyProfiles").toUpperCase(),
|
title: transI18n("copyProfiles"),
|
||||||
isOpen: isOpenCopy,
|
isOpen: isOpenCopy,
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
setIsOpenCopy(false)
|
setIsOpenCopy(false)
|
||||||
@@ -179,7 +214,7 @@ export default function ActionBar() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "avatars_modal",
|
id: "avatars_modal",
|
||||||
title: transI18n("avatars").toUpperCase(),
|
title: transI18n("avatars"),
|
||||||
isOpen: isOpenAvatars,
|
isOpen: isOpenAvatars,
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
setIsOpenAvatars(false)
|
setIsOpenAvatars(false)
|
||||||
@@ -208,238 +243,268 @@ export default function ActionBar() {
|
|||||||
return () => window.removeEventListener('keydown', handleEscKey);
|
return () => window.removeEventListener('keydown', handleEscKey);
|
||||||
}, [isOpenCopy, isOpenCreateProfile, isOpenAvatars]);
|
}, [isOpenCopy, isOpenCreateProfile, isOpenAvatars]);
|
||||||
|
|
||||||
|
const navTabs = [
|
||||||
|
{ label: transI18n("characterInformation"), path: "", icon: User },
|
||||||
|
{ label: transI18n("relics"), path: "relics-info", icon: Shield },
|
||||||
|
{ label: transI18n("eidolons"), path: "eidolons-info", icon: Sparkles },
|
||||||
|
{ label: transI18n("skills"), path: "skills-info", icon: Zap },
|
||||||
|
{ label: transI18n("showcaseCard"), path: "showcase-card", icon: ImageIcon },
|
||||||
|
];
|
||||||
|
|
||||||
|
const currentActivePath = pathname?.replace(/^\//, '') || "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full px-4 pb-4 bg-base-200">
|
<div className="w-full bg-transparent px-3 sm:px-4 py-2 space-y-2">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 items-center justify-items-center">
|
{/* Top Row: Character Identity + Profile Selector + Connect PS */}
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2.5">
|
||||||
|
{/* Character Identity */}
|
||||||
|
{avatarSelected && (
|
||||||
|
<div className="flex items-center gap-2 h-8.5 px-3 rounded-xl bg-base-200/80 border border-base-content/10 text-xs sm:text-sm font-semibold shadow-xs">
|
||||||
|
<Image
|
||||||
|
src={`${process.env.CDN_URL}/${damageType?.[avatarSelected?.DamageType]?.Icon}`}
|
||||||
|
alt="damage type"
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
className="h-5 w-5 object-contain"
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
/>
|
||||||
|
<span className="text-base-content/85 font-medium">{transI18n(avatarSelected.BaseType.toLowerCase())}</span>
|
||||||
|
<span className="text-base-content/30">/</span>
|
||||||
|
<ParseText
|
||||||
|
locale={locale}
|
||||||
|
text={getNameChar(locale, transI18n, avatarSelected).toWellFormed()}
|
||||||
|
className="font-bold text-primary"
|
||||||
|
/>
|
||||||
|
<span className="text-[11px] font-mono text-base-content/60 font-semibold">#{avatarSelected.ID}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col justify-center w-full">
|
{/* Profile Selector & Quick Actions */}
|
||||||
<div className="flex flex-wrap items-center gap-2 ">
|
<div className="flex items-center h-8.5 rounded-xl bg-base-200/80 border border-base-content/10 p-0.5 shadow-xs">
|
||||||
<div className="flex flex-wrap items-center h-full opacity-80 lg:hover:opacity-100 cursor-pointer text-base md:text-lg lg:text-xl">
|
<div className="dropdown dropdown-bottom dropdown-start h-full flex items-center">
|
||||||
{avatarSelected && (
|
<div
|
||||||
<div className="flex flex-wrap items-center justify-start h-full w-full">
|
tabIndex={0}
|
||||||
<Image
|
role="button"
|
||||||
src={`${process.env.CDN_URL}/${damageType?.[avatarSelected?.DamageType]?.Icon}`}
|
className="btn btn-sm btn-ghost gap-1.5 h-7 min-h-7 font-bold rounded-lg hover:bg-base-300/80 px-2"
|
||||||
alt={'damage type'}
|
>
|
||||||
unoptimized
|
<span className="text-[11px] uppercase tracking-wider text-base-content/60 font-semibold">{transI18n("profile")}:</span>
|
||||||
crossOrigin="anonymous"
|
<span className="truncate max-w-28 sm:max-w-40 text-xs text-base-content font-bold">
|
||||||
className="h-10 w-10 object-contain"
|
{profileCurrent?.profile_name || "Default Profile"}
|
||||||
width={100}
|
</span>
|
||||||
height={100}
|
<svg className="w-3.5 h-3.5 opacity-60 ml-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
/>
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
<p className="text-center font-bold text-lg">
|
</svg>
|
||||||
{transI18n(avatarSelected.BaseType.toLowerCase())}
|
|
||||||
</p>
|
|
||||||
<div className="text-center font-bold text-lg">{" / "}</div>
|
|
||||||
<ParseText
|
|
||||||
locale={locale}
|
|
||||||
text={getNameChar(locale, transI18n, avatarSelected).toWellFormed()}
|
|
||||||
className={"font-bold text-lg"}
|
|
||||||
/>
|
|
||||||
<div className="text-center italic text-sm ml-2"> {`(${avatarSelected.ID})`}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ul className="dropdown-content z-50 menu p-2 shadow-2xl bg-base-100 rounded-2xl min-w-60 mt-2 border border-base-300 max-h-72 overflow-y-auto space-y-1">
|
||||||
|
{listProfile.map((profile, index) => (
|
||||||
|
<li key={index} className="flex flex-row items-center justify-between rounded-xl px-1 hover:bg-base-200">
|
||||||
|
<button
|
||||||
|
className="flex-1 text-left py-2 px-2 text-xs font-medium truncate"
|
||||||
|
onClick={() => handleProfileSelect(index)}
|
||||||
|
>
|
||||||
|
<span className="truncate">{profile.profile_name}</span>
|
||||||
|
{index === 0 && (
|
||||||
|
<span className="text-[10px] text-base-content/50 ml-1.5 font-mono">({transI18n("default")})</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{index !== 0 && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost btn-xs btn-circle text-warning hover:bg-warning/20"
|
||||||
|
onClick={() => {
|
||||||
|
setFormState("EDIT");
|
||||||
|
setProfileName(profile.profile_name)
|
||||||
|
setProfileEdit(index)
|
||||||
|
setIsOpenCreateProfile(true)
|
||||||
|
handleShow("update_profile_modal")
|
||||||
|
}}
|
||||||
|
title="Edit Profile"
|
||||||
|
>
|
||||||
|
<Edit2 className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost btn-xs btn-circle text-error hover:bg-error/20"
|
||||||
|
onClick={(e) => handleDeleteProfile(index, e)}
|
||||||
|
title="Delete Profile"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<li className="border-t border-base-300 pt-1 mt-1">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpenCopy(true)
|
||||||
|
setResetData(baseType, damageType)
|
||||||
|
handleShow("copy_profile_modal")
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 py-2 px-3 text-xs font-semibold text-primary hover:bg-primary/10 rounded-xl"
|
||||||
|
>
|
||||||
|
<Copy className="w-3.5 h-3.5" />
|
||||||
|
{transI18n("copyProfiles")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpenCreateProfile(true)
|
||||||
|
setFormState("CREATE");
|
||||||
|
setProfileName("")
|
||||||
|
handleShow("update_profile_modal")
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 py-2 px-3 text-xs font-semibold text-primary hover:bg-primary/10 rounded-xl"
|
||||||
|
>
|
||||||
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
{transI18n("addNewProfile")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
{/* Quick Profile Actions */}
|
||||||
<span className="text-base opacity-70 font-bold w-16">{transI18n("profile")}:</span>
|
<div className="flex items-center border-l border-base-content/10 pl-1 pr-0.5 gap-0.5 h-5">
|
||||||
<div className="dropdown dropdown-center md:dropdown-start">
|
<button
|
||||||
<div
|
onClick={() => {
|
||||||
tabIndex={0}
|
setIsOpenCopy(true)
|
||||||
role="button"
|
setResetData(baseType, damageType)
|
||||||
className="btn btn-warning border-info btn-soft gap-1"
|
handleShow("copy_profile_modal")
|
||||||
>
|
}}
|
||||||
<span className="truncate max-w-48 font-bold">
|
className="btn btn-ghost btn-xs btn-circle h-6 w-6 min-h-6 text-base-content/60 hover:text-primary hover:bg-primary/10"
|
||||||
{profileCurrent?.profile_name}
|
title={transI18n("copyProfiles")}
|
||||||
</span>
|
>
|
||||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<Copy className="w-3.5 h-3.5" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
</button>
|
||||||
</svg>
|
<button
|
||||||
</div>
|
onClick={() => {
|
||||||
|
setIsOpenCreateProfile(true)
|
||||||
|
setFormState("CREATE");
|
||||||
|
setProfileName("")
|
||||||
|
handleShow("update_profile_modal")
|
||||||
|
}}
|
||||||
|
className="btn btn-ghost btn-xs btn-circle h-6 w-6 min-h-6 text-base-content/60 hover:text-primary hover:bg-primary/10"
|
||||||
|
title={transI18n("addNewProfile")}
|
||||||
|
>
|
||||||
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ul className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box min-w-max w-full mt-1 border border-base-300 max-h-60 overflow-y-auto">
|
{/* Desktop Connect PS Button */}
|
||||||
{listProfile.map((profile, index) => (
|
<div className="hidden sm:flex items-center gap-2 ml-auto tooltip tooltip-bottom" data-tip={isConnectPS ? transI18n("sync") : transI18n("connectPs")}>
|
||||||
<li key={index} className="grid grid-cols-12">
|
<button
|
||||||
<button
|
onClick={handleConnectOrSyncPS}
|
||||||
className={`col-span-8 btn btn-ghost`}
|
className={`btn btn-sm h-8.5 min-h-8.5 rounded-xl px-3.5 gap-1.5 font-bold shadow-xs whitespace-nowrap active-press ${
|
||||||
onClick={() => handleProfileSelect(index)}
|
isConnectPS
|
||||||
>
|
? "btn-outline btn-accent"
|
||||||
<span className="flex-1 truncate text-left">
|
: "btn-primary shadow-sm"
|
||||||
{profile.profile_name}
|
}`}
|
||||||
{index === 0 && (
|
>
|
||||||
<span className="text-xs text-base-content/60 ml-1">({transI18n("default")})</span>
|
<RefreshCw className={`w-3.5 h-3.5 ${isConnectPS ? "text-accent" : ""}`} />
|
||||||
)}
|
<span className="text-xs">{isConnectPS ? transI18n("sync") : transI18n("connectPs")}</span>
|
||||||
</span>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</button>
|
{/* Mobile-Only Action Row (< 640px): [Avatars Button] & [Actions Dropdown] */}
|
||||||
{index !== 0 && (
|
<div className="grid grid-cols-2 sm:hidden gap-2 w-full pt-1">
|
||||||
<>
|
<button
|
||||||
<button
|
onClick={() => {
|
||||||
className="col-span-2 flex items-center justify-center text-lg text-warning hover:bg-warning/20 z-20 w-full h-full"
|
setIsOpenAvatars(true);
|
||||||
onClick={() => {
|
handleShow("avatars_modal");
|
||||||
setFormState("EDIT");
|
}}
|
||||||
setProfileName(profile.profile_name)
|
className="btn btn-warning btn-sm w-full font-bold shadow-xs"
|
||||||
setProfileEdit(index)
|
>
|
||||||
setIsOpenCreateProfile(true)
|
{transI18n("avatars")}
|
||||||
handleShow("update_profile_modal")
|
</button>
|
||||||
}}
|
|
||||||
title="Edit Profile"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536M4 20h4l10.293-10.293a1 1 0 000-1.414l-2.586-2.586a1 1 0 00-1.414 0L4 16v4z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="col-span-2 flex items-center justify-center text-error hover:bg-error/20 z-20 w-full h-full text-lg"
|
|
||||||
onClick={(e) => {
|
|
||||||
handleDeleteProfile(index, e)
|
|
||||||
}}
|
|
||||||
title="Delete Profile"
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M9 7V4h6v3m5 0H4" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<li className="border-t border-base-300 mt-2 pt-2 z-10">
|
<div className="dropdown dropdown-end w-full">
|
||||||
|
<label tabIndex={0} className="btn btn-info btn-sm w-full font-bold shadow-xs">
|
||||||
|
{transI18n("actions")}
|
||||||
|
</label>
|
||||||
|
<ul
|
||||||
|
tabIndex={0}
|
||||||
|
className="dropdown-content z-50 menu p-2 shadow-2xl bg-base-100 rounded-2xl w-56 mt-2 border border-base-300 space-y-1"
|
||||||
|
>
|
||||||
|
{navTabs.map((tab) => {
|
||||||
|
const Icon = tab.icon;
|
||||||
|
return (
|
||||||
|
<li key={tab.path}>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => actionMove(tab.path)}
|
||||||
setIsOpenCopy(true)
|
className="flex items-center gap-2 py-2 px-3 text-xs font-semibold"
|
||||||
setResetData(baseType, damageType)
|
|
||||||
handleShow("copy_profile_modal")
|
|
||||||
}}
|
|
||||||
className="btn btn-ghost flex justify-start px-3 py-2 h-full w-full hover:bg-base-200 cursor-pointer text-primary z-20"
|
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<Icon className="w-4 h-4" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
<span>{tab.label}</span>
|
||||||
</svg>
|
|
||||||
{transI18n("copyProfiles")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-ghost flex justify-start px-3 py-2 h-full w-full hover:bg-base-200 cursor-pointer text-primary z-20"
|
|
||||||
onClick={() => {
|
|
||||||
|
|
||||||
setIsOpenCreateProfile(true)
|
|
||||||
setFormState("CREATE");
|
|
||||||
setProfileName("")
|
|
||||||
handleShow("update_profile_modal")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
|
||||||
</svg>
|
|
||||||
{transI18n("addNewProfile")}
|
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
);
|
||||||
</div>
|
})}
|
||||||
<div className=" grid grid-cols-2 w-full sm:hidden gap-2">
|
<li className="border-t border-base-300 pt-1 mt-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={handleConnectOrSyncPS}
|
||||||
setIsOpenAvatars(true)
|
className="btn btn-primary btn-sm w-full font-bold mt-1"
|
||||||
handleShow("avatars_modal")
|
>
|
||||||
}}
|
<RefreshCw className="w-3.5 h-3.5" />
|
||||||
className="col-span-1 btn btn-warning btn-sm w-full">
|
<span>{isConnectPS ? transI18n("sync") : transI18n("connectPs")}</span>
|
||||||
{transI18n("avatars")}
|
|
||||||
</button>
|
</button>
|
||||||
<div className="col-span-1 dropdown dropdown-center w-full">
|
</li>
|
||||||
<label tabIndex={0} className="btn btn-info btn-sm w-full">
|
</ul>
|
||||||
{transI18n("actions")}
|
|
||||||
</label>
|
|
||||||
<ul
|
|
||||||
tabIndex={0}
|
|
||||||
className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box min-w-max w-full mt-1 border border-base-300 max-h-60 overflow-y-auto"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<button onClick={() => actionMove('')}>
|
|
||||||
{transI18n("characterInformation")}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button onClick={() => actionMove('relics-info')}>
|
|
||||||
{transI18n("relics")}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button onClick={() => actionMove('eidolons-info')}>
|
|
||||||
{transI18n("eidolons")}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button onClick={() => actionMove('skills-info')}>
|
|
||||||
{transI18n("skills")}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button onClick={() => actionMove('showcase-card')}>
|
|
||||||
{transI18n("showcaseCard")}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
|
|
||||||
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="hidden sm:grid grid-cols-3 gap-2 w-full">
|
{/* Navigation Tabs Row (>= 640px) */}
|
||||||
<button className="btn btn-success btn-sm" onClick={() => actionMove("")}>
|
<div className="hidden sm:block w-full bg-base-200/80 border border-base-content/10 rounded-xl p-1 shadow-xs overflow-x-auto scrollbar-none">
|
||||||
{transI18n("characterInformation")}
|
<div className="flex sm:grid sm:grid-cols-5 w-full min-w-max gap-1">
|
||||||
</button>
|
{navTabs.map((tab) => {
|
||||||
<button className="btn btn-success btn-sm" onClick={() => actionMove("relics-info")}>
|
const isActive = currentActivePath === tab.path;
|
||||||
{transI18n("relics")}
|
const Icon = tab.icon;
|
||||||
</button>
|
return (
|
||||||
<button className="btn btn-success btn-sm" onClick={() => actionMove("eidolons-info")}>
|
<button
|
||||||
{transI18n("eidolons")}
|
key={tab.path}
|
||||||
</button>
|
onClick={() => actionMove(tab.path)}
|
||||||
<button className="btn btn-success btn-sm" onClick={() => actionMove("skills-info")}>
|
className={`flex items-center justify-center gap-2 py-2 px-3 rounded-lg text-xs font-bold transition-all duration-200 whitespace-nowrap active-press ${
|
||||||
{transI18n("skills")}
|
isActive
|
||||||
</button>
|
? "bg-primary text-primary-content shadow-xs"
|
||||||
<button className="btn btn-success btn-sm" onClick={() => actionMove("showcase-card")}>
|
: "text-base-content/75 hover:text-base-content hover:bg-base-100"
|
||||||
{transI18n("showcaseCard")}
|
}`}
|
||||||
</button>
|
>
|
||||||
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
|
<Icon className="w-4 h-4 shrink-0" />
|
||||||
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
|
<span>{tab.label}</span>
|
||||||
</button>
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modals */}
|
||||||
{modalConfigs.map(({ id, title, onClose, content }) => (
|
{modalConfigs.map(({ id, title, onClose, content }) => (
|
||||||
<dialog key={id} id={id} className="modal">
|
<dialog key={id} id={id} className="modal">
|
||||||
<div className="modal-box w-11/12 max-w-[90%] max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
|
<div className="modal-box w-11/12 max-w-[92vw] sm:max-w-[88vw] max-h-[90vh] bg-base-100 text-base-content border border-base-300 shadow-2xl rounded-2xl p-0 overflow-hidden">
|
||||||
<div className="sticky top-0 z-10">
|
<div className="sticky top-0 z-20 flex items-center justify-between px-6 py-4 border-b border-base-300 bg-base-100">
|
||||||
<motion.button
|
<h3 className="font-bold text-xl tracking-tight text-primary">
|
||||||
whileHover={{ scale: 1.1, rotate: 90 }}
|
{title}
|
||||||
transition={{ duration: 0.2 }}
|
</h3>
|
||||||
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
|
<button
|
||||||
|
className="btn btn-sm btn-circle btn-ghost text-base-content/60 hover:text-base-content hover:bg-base-200 transition-colors"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</motion.button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
<div className="p-4 sm:p-6 overflow-y-auto max-h-[calc(90vh-5rem)]">
|
||||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
{content}
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
{content}
|
|
||||||
</div>
|
</div>
|
||||||
|
<form method="dialog" className="modal-backdrop">
|
||||||
|
<button onClick={onClose}>close</button>
|
||||||
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,8 @@ import { useTranslations } from "next-intl"
|
|||||||
import useDetailDataStore from "@/stores/detailDataStore"
|
import useDetailDataStore from "@/stores/detailDataStore"
|
||||||
import useCurrentDataStore from '@/stores/currentDataStore';
|
import useCurrentDataStore from '@/stores/currentDataStore';
|
||||||
import { calcRarity, getNameChar } from "@/helper"
|
import { calcRarity, getNameChar } from "@/helper"
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
import { Search, X } from "lucide-react"
|
||||||
|
|
||||||
export default function AvatarBar({ onClose }: { onClose?: () => void }) {
|
export default function AvatarBar({ onClose }: { onClose?: () => void }) {
|
||||||
const {
|
const {
|
||||||
@@ -18,10 +20,29 @@ export default function AvatarBar({ onClose }: { onClose?: () => void }) {
|
|||||||
setMapAvatarElementActive,
|
setMapAvatarElementActive,
|
||||||
setMapAvatarPathActive,
|
setMapAvatarPathActive,
|
||||||
setSkillIDSelected,
|
setSkillIDSelected,
|
||||||
} = useCurrentDataStore()
|
} = useCurrentDataStore(
|
||||||
const { mapAvatar, baseType, damageType } = useDetailDataStore()
|
useShallow((state) => ({
|
||||||
|
avatarSearch: state.avatarSearch,
|
||||||
|
mapAvatarElementActive: state.mapAvatarElementActive,
|
||||||
|
mapAvatarPathActive: state.mapAvatarPathActive,
|
||||||
|
setAvatarSearch: state.setAvatarSearch,
|
||||||
|
setAvatarSelected: state.setAvatarSelected,
|
||||||
|
setMapAvatarElementActive: state.setMapAvatarElementActive,
|
||||||
|
setMapAvatarPathActive: state.setMapAvatarPathActive,
|
||||||
|
setSkillIDSelected: state.setSkillIDSelected,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { mapAvatar, baseType, damageType } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
mapAvatar: state.mapAvatar,
|
||||||
|
baseType: state.baseType,
|
||||||
|
damageType: state.damageType,
|
||||||
|
}))
|
||||||
|
)
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
const {locale} = useLocaleStore()
|
const { locale } = useLocaleStore(
|
||||||
|
useShallow((state) => ({ locale: state.locale }))
|
||||||
|
)
|
||||||
|
|
||||||
const listAvatar = useMemo(() => {
|
const listAvatar = useMemo(() => {
|
||||||
if (!mapAvatar || !locale || !transI18n) return []
|
if (!mapAvatar || !locale || !transI18n) return []
|
||||||
@@ -53,87 +74,125 @@ export default function AvatarBar({ onClose }: { onClose?: () => void }) {
|
|||||||
return list
|
return list
|
||||||
}, [mapAvatar, mapAvatarElementActive, mapAvatarPathActive, avatarSearch, locale, transI18n])
|
}, [mapAvatar, mapAvatarElementActive, mapAvatarPathActive, avatarSearch, locale, transI18n])
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-flow-row h-full auto-rows-max w-full">
|
<div className="flex flex-col h-full gap-2 p-2">
|
||||||
<div className="h-full rounded-lg mx-2 py-2">
|
{/* Compact Search Bar */}
|
||||||
<div className="container">
|
<label className="input input-sm input-bordered flex items-center gap-2 bg-base-200/90 border-base-300 rounded-xl w-full h-8.5 px-3 shadow-xs focus-within:border-primary/60 transition-colors">
|
||||||
<div className="flex flex-col h-full gap-2">
|
<Search className="w-4 h-4 text-base-content/70 shrink-0" />
|
||||||
<div className="flex flex-col gap-2">
|
<input
|
||||||
<div className="flex justify-center">
|
type="text"
|
||||||
<input type="text"
|
placeholder={transI18n("placeholderCharacter")}
|
||||||
placeholder={transI18n("placeholderCharacter")}
|
className="grow text-xs bg-transparent border-none outline-none focus:outline-none placeholder:text-base-content/50"
|
||||||
className="input input-bordered input-primary w-full"
|
value={avatarSearch}
|
||||||
value={avatarSearch}
|
onChange={(e) => setAvatarSearch(e.target.value)}
|
||||||
onChange={(e) => setAvatarSearch(e.target.value)}
|
/>
|
||||||
|
{avatarSearch && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setAvatarSearch("")}
|
||||||
|
className="text-base-content/50 hover:text-base-content"
|
||||||
|
>
|
||||||
|
<X className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Compact Filter Section */}
|
||||||
|
<div className="p-1 rounded-xl bg-base-200/80 border border-base-content/15 space-y-1 shadow-xs">
|
||||||
|
{/* Element Filters - 7 Elements on 1 Row (gap-0.5) */}
|
||||||
|
<div className="grid grid-cols-7 gap-0.5">
|
||||||
|
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => {
|
||||||
|
const isActive = !!mapAvatarElementActive[key];
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setMapAvatarElementActive({ ...mapAvatarElementActive, [key]: !mapAvatarElementActive[key] })
|
||||||
|
}}
|
||||||
|
className={`h-8.5 w-full rounded-lg transition-all duration-150 active-press flex items-center justify-center p-0 ${
|
||||||
|
isActive
|
||||||
|
? "bg-primary/20 text-primary ring-1.5 ring-primary scale-105 z-10 shadow-xs"
|
||||||
|
: "hover:bg-base-100 opacity-90 hover:opacity-100"
|
||||||
|
}`}
|
||||||
|
title={value.DamageTypeName || key}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||||
|
alt={key}
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
className="w-6.5 h-6.5 object-contain drop-shadow-xs"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
/>
|
/>
|
||||||
</div>
|
</button>
|
||||||
<div className="grid grid-cols-7 sm:grid-cols-4 lg:grid-cols-7 mb-1 mx-1 gap-2 w-full max-h-[17vh] min-h-[5vh] overflow-y-auto">
|
);
|
||||||
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
|
})}
|
||||||
<div
|
|
||||||
key={key}
|
|
||||||
onClick={() => {
|
|
||||||
setMapAvatarElementActive({ ...mapAvatarElementActive, [key]: !mapAvatarElementActive[key] })
|
|
||||||
}}
|
|
||||||
className="hover:bg-gray-600 grid items-center justify-items-center cursor-pointer rounded-md shadow-lg"
|
|
||||||
style={{
|
|
||||||
backgroundColor: mapAvatarElementActive[key] ? "#374151" : "#6B7280"
|
|
||||||
}}>
|
|
||||||
<Image
|
|
||||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
|
||||||
alt={key}
|
|
||||||
unoptimized
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
|
|
||||||
width={200}
|
|
||||||
height={200} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-9 sm:grid-cols-5 lg:grid-cols-9 mb-1 mx-1 gap-2 overflow-y-auto w-full max-h-[17vh] min-h-[5vh]">
|
|
||||||
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
|
|
||||||
<div
|
|
||||||
key={key}
|
|
||||||
onClick={() => {
|
|
||||||
setMapAvatarPathActive({ ...mapAvatarPathActive, [key]: !mapAvatarPathActive[key] })
|
|
||||||
}}
|
|
||||||
className="hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-lg cursor-pointer"
|
|
||||||
style={{
|
|
||||||
backgroundColor: mapAvatarPathActive[key] ? "#374151" : "#6B7280"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
|
|
||||||
<Image
|
|
||||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
|
||||||
unoptimized
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
alt={key}
|
|
||||||
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
|
|
||||||
width={200}
|
|
||||||
height={200} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start h-full">
|
|
||||||
<ul className="grid grid-cols-3 sm:grid-cols-2 lg:grid-cols-3 gap-2 w-full h-[65vh] overflow-y-scroll overflow-x-hidden">
|
|
||||||
{listAvatar.map((item, index) => (
|
|
||||||
<div key={index} onClick={() => {
|
|
||||||
setAvatarSelected(item);
|
|
||||||
setSkillIDSelected(null)
|
|
||||||
if (onClose) onClose()
|
|
||||||
}}>
|
|
||||||
<CharacterCard data={item} />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Path Filters - 9 Paths on 1 Row (gap-0.5) */}
|
||||||
|
<div className="grid grid-cols-9 gap-0.5">
|
||||||
|
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => {
|
||||||
|
const isActive = !!mapAvatarPathActive[key];
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setMapAvatarPathActive({ ...mapAvatarPathActive, [key]: !mapAvatarPathActive[key] })
|
||||||
|
}}
|
||||||
|
className={`h-8 w-full rounded-lg transition-all duration-150 active-press flex items-center justify-center p-0 ${
|
||||||
|
isActive
|
||||||
|
? "bg-primary/20 ring-1.5 ring-primary scale-105 z-10 shadow-xs"
|
||||||
|
: "hover:bg-base-100 opacity-90 hover:opacity-100"
|
||||||
|
}`}
|
||||||
|
title={value.BaseTypeName || key}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
alt={key}
|
||||||
|
className={`w-5.5 h-5.5 object-contain ${
|
||||||
|
isActive ? "theme-path-icon-active" : "theme-path-icon"
|
||||||
|
}`}
|
||||||
|
width={28}
|
||||||
|
height={28}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Character Count & Grid List */}
|
||||||
|
<div className="flex items-center justify-between text-xs font-semibold text-base-content/80 px-1 py-0.5">
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse"></span>
|
||||||
|
<span>{transI18n("character")}</span>
|
||||||
|
</span>
|
||||||
|
<span className="font-mono text-xs font-bold px-2 py-0.5 rounded-md bg-primary/10 text-primary border border-primary/20">
|
||||||
|
{listAvatar.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto pr-0.5 scrollbar-thin">
|
||||||
|
<ul className="grid grid-cols-3 sm:grid-cols-2 lg:grid-cols-3 gap-1.5 auto-rows-fr">
|
||||||
|
{listAvatar.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.ID}
|
||||||
|
className="h-full flex flex-col"
|
||||||
|
onClick={() => {
|
||||||
|
setAvatarSelected(item);
|
||||||
|
setSkillIDSelected(null)
|
||||||
|
if (onClose) onClose()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CharacterCard data={item} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
+387
-441
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
import useUserDataStore from "@/stores/userDataStore";
|
import useUserDataStore from "@/stores/userDataStore";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import LightconeBar from '../lightconeBar'
|
import LightconeBar from '../lightconeBar'
|
||||||
import { calcPromotion, calcRarity, replaceByParam } from '@/helper';
|
import { calcPromotion, calcRarity, replaceByParam } from '@/helper';
|
||||||
import { getSkillTree } from '@/helper/getSkillTree';
|
import { getSkillTree } from '@/helper/getSkillTree';
|
||||||
@@ -15,20 +14,62 @@ import Image from 'next/image';
|
|||||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||||
import useDetailDataStore from "@/stores/detailDataStore";
|
import useDetailDataStore from "@/stores/detailDataStore";
|
||||||
import { getLocaleName } from "@/helper/getName";
|
import { getLocaleName } from "@/helper/getName";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
|
||||||
export default function AvatarInfo() {
|
export default function AvatarInfo() {
|
||||||
const { avatarSelected, setResetDataLightcone } = useCurrentDataStore()
|
const { avatarSelected, setResetDataLightcone } = useCurrentDataStore(
|
||||||
const { avatars, setAvatars, setAvatar } = useUserDataStore()
|
useShallow((state) => ({
|
||||||
const { isOpenLightcone, setIsOpenLightcone } = useModelStore()
|
avatarSelected: state.avatarSelected,
|
||||||
const { mapLightCone, baseType } = useDetailDataStore()
|
setResetDataLightcone: state.setResetDataLightcone,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { avatars, setAvatars, setAvatar } = useUserDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
avatars: state.avatars,
|
||||||
|
setAvatars: state.setAvatars,
|
||||||
|
setAvatar: state.setAvatar,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { isOpenLightcone, setIsOpenLightcone } = useModelStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
isOpenLightcone: state.isOpenLightcone,
|
||||||
|
setIsOpenLightcone: state.setIsOpenLightcone,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { mapLightCone, baseType } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
mapLightCone: state.mapLightCone,
|
||||||
|
baseType: state.baseType,
|
||||||
|
}))
|
||||||
|
)
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
const { locale } = useLocaleStore();
|
const { locale } = useLocaleStore(
|
||||||
|
useShallow((state) => ({ locale: state.locale }))
|
||||||
|
);
|
||||||
|
|
||||||
const lightcone = useMemo(() => {
|
const lightcone = useMemo(() => {
|
||||||
if (!avatarSelected) return null;
|
if (!avatarSelected) return null;
|
||||||
const avatar = avatars[avatarSelected.ID];
|
const avatar = avatars[avatarSelected.ID];
|
||||||
return avatar?.profileList[avatar.profileSelect]?.lightcone || null;
|
return avatar?.profileList?.[avatar?.profileSelect]?.lightcone ?? avatar?.profileList?.[0]?.lightcone ?? null;
|
||||||
}, [avatarSelected, avatars]);
|
}, [avatarSelected, avatars]);
|
||||||
|
|
||||||
|
const handleUpdateLightcone = (updater: (lc: typeof lightcone) => typeof lightcone) => {
|
||||||
|
if (!avatarSelected || !avatars[avatarSelected.ID]) return;
|
||||||
|
const avatar = avatars[avatarSelected.ID];
|
||||||
|
const profileIndex = avatar.profileSelect;
|
||||||
|
const currentProfile = avatar.profileList?.[profileIndex];
|
||||||
|
if (!currentProfile) return;
|
||||||
|
const newProfileList = [...avatar.profileList];
|
||||||
|
newProfileList[profileIndex] = {
|
||||||
|
...currentProfile,
|
||||||
|
lightcone: updater(currentProfile.lightcone),
|
||||||
|
};
|
||||||
|
setAvatar({
|
||||||
|
...avatar,
|
||||||
|
profileList: newProfileList,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const lightconeDetail = useMemo(() => {
|
const lightconeDetail = useMemo(() => {
|
||||||
if (!mapLightCone || !lightcone?.item_id) return null;
|
if (!mapLightCone || !lightcone?.item_id) return null;
|
||||||
return mapLightCone?.[lightcone.item_id.toString()] || null;
|
return mapLightCone?.[lightcone.item_id.toString()] || null;
|
||||||
@@ -70,460 +111,365 @@ export default function AvatarInfo() {
|
|||||||
}, [isOpenLightcone]);
|
}, [isOpenLightcone]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-base-100 max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
|
<div className="w-full space-y-4">
|
||||||
{avatarSelected && avatars[avatarSelected?.ID.toString() || ""] && (
|
{avatarSelected && avatars[avatarSelected?.ID.toString() || ""] && (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 w-full">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6 w-full items-start">
|
||||||
<div className="m-2 min-h-96">
|
{/* Left Column: Character Configuration */}
|
||||||
<div className="container">
|
<div className="glass-card rounded-2xl p-5 sm:p-6 border border-base-content/10 shadow-xs flex flex-col justify-between">
|
||||||
<div className="card bg-base-200 shadow-xl">
|
<div>
|
||||||
<div className="card-body">
|
{/* Card Header */}
|
||||||
{/* Header */}
|
<div className="flex items-center justify-between pb-4 mb-2 border-b border-base-content/10">
|
||||||
<div className="flex items-center justify-between mb-6">
|
<h2 className="text-base sm:text-lg font-bold flex items-center gap-2.5">
|
||||||
<h2 className="card-title text-2xl font-bold flex items-center gap-2">
|
<div className="w-1.5 h-5 bg-linear-to-b from-primary to-accent rounded-full"></div>
|
||||||
<div className="w-2 h-8 bg-linear-to-b from-success to-success/50 rounded-full"></div>
|
<span className="bg-clip-text text-transparent bg-linear-to-r from-primary to-accent font-bold tracking-tight">
|
||||||
{transI18n("characterSettings")}
|
{transI18n("characterSettings")}
|
||||||
</h2>
|
</span>
|
||||||
</div>
|
</h2>
|
||||||
|
<span className="text-xs font-mono font-semibold px-2.5 py-1 rounded-md bg-primary/10 text-primary border border-primary/20">
|
||||||
<div className="space-y-6">
|
ID: #{avatarSelected.ID}
|
||||||
{/* Level Control */}
|
</span>
|
||||||
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10">
|
|
||||||
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
||||||
<div className="w-2 h-6 bg-linear-to-b from-info to-info/50 rounded-full"></div>
|
|
||||||
{transI18n("levelConfiguration")}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<div className="form-control">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-medium">{transI18n("characterLevel")}</span>
|
|
||||||
<span className="label-text-alt text-info font-mono">{avatars[avatarSelected?.ID.toString() || ""]?.level}/80</span>
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="80"
|
|
||||||
value={avatars[avatarSelected?.ID.toString() || ""]?.level}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newLevel = Math.min(80, Math.max(1, parseInt(e.target.value) || 1));
|
|
||||||
|
|
||||||
setAvatars({ ...avatars, [avatarSelected?.ID.toString() || ""]: { ...avatars[avatarSelected?.ID.toString() || ""], level: newLevel, promotion: calcPromotion(newLevel) } });
|
|
||||||
}}
|
|
||||||
className="input input-bordered w-full pr-16 font-mono"
|
|
||||||
placeholder={transI18n("placeholderLevel")}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
onClick={() => {
|
|
||||||
setAvatars({ ...avatars, [avatarSelected?.ID.toString() || ""]: { ...avatars[avatarSelected?.ID.toString() || ""], level: 80, promotion: calcPromotion(80) } });
|
|
||||||
}}
|
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-base-content/60 cursor-pointer">
|
|
||||||
<span className="text-sm">{transI18n("max")}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="1"
|
|
||||||
max="80"
|
|
||||||
value={avatars[avatarSelected?.ID.toString() || ""]?.level}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newLevel = Math.min(80, Math.max(1, parseInt(e.target.value) || 1));
|
|
||||||
setAvatars({ ...avatars, [avatarSelected?.ID.toString() || ""]: { ...avatars[avatarSelected?.ID.toString() || ""], level: newLevel, promotion: calcPromotion(newLevel) } });
|
|
||||||
}}
|
|
||||||
className="range range-info range-sm w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Energy Control */}
|
|
||||||
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10">
|
|
||||||
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
||||||
<div className="w-2 h-6 bg-linear-to-b from-warning to-warning/50 rounded-full"></div>
|
|
||||||
{transI18n("ultimateEnergy")}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<div className="form-control">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-medium">{transI18n("currentEnergy")}</span>
|
|
||||||
<span className="label-text text-warning font-mono">
|
|
||||||
{Math.round(avatars[avatarSelected?.ID.toString() || ""]?.sp_value)}/{avatars[avatarSelected?.ID.toString() || ""]?.sp_max}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max={avatars[avatarSelected?.ID.toString() || ""]?.sp_max}
|
|
||||||
value={avatars[avatarSelected?.ID.toString() || ""]?.sp_value}
|
|
||||||
onChange={(e) => {
|
|
||||||
if (!avatars[avatarSelected?.ID.toString() || ""]?.can_change_sp) return
|
|
||||||
const newSpValue = Math.min(avatars[avatarSelected?.ID.toString() || ""]?.sp_max, Math.max(0, parseInt(e.target.value) || 0));
|
|
||||||
setAvatars({ ...avatars, [avatarSelected?.ID.toString() || ""]: { ...avatars[avatarSelected?.ID.toString() || ""], sp_value: newSpValue } });
|
|
||||||
}}
|
|
||||||
className="range range-warning range-sm w-full"
|
|
||||||
/>
|
|
||||||
<div className="flex justify-between text-sm text-base-content/60 mt-1">
|
|
||||||
<span>0%</span>
|
|
||||||
<span className="font-mono text-warning">{((avatars[avatarSelected?.ID.toString() || ""]?.sp_value / avatars[avatarSelected?.ID.toString() || ""]?.sp_max) * 100).toFixed(1)}%</span>
|
|
||||||
<span>100%</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-3">
|
|
||||||
<button
|
|
||||||
className="btn btn-sm btn-outline btn-warning"
|
|
||||||
onClick={() => {
|
|
||||||
if (!avatars[avatarSelected?.ID.toString() || ""]?.can_change_sp) return
|
|
||||||
const newSpValue = Math.ceil(avatars[avatarSelected?.ID.toString() || ""]?.sp_max / 2);
|
|
||||||
const newAvatar = { ...avatars[avatarSelected?.ID.toString() || ""], sp_value: newSpValue }
|
|
||||||
setAvatar(newAvatar)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{transI18n("setTo50")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Technique Toggle */}
|
|
||||||
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10">
|
|
||||||
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
||||||
<div className="w-2 h-6 bg-linear-to-b from-accent to-accent/50 rounded-full"></div>
|
|
||||||
{transI18n("battleConfiguration")}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<div className="form-control">
|
|
||||||
<label className="grid grid-cols-1 gap-2 sm:grid-cols-2 label cursor-pointer">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<svg className="w-5 h-5 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
||||||
</svg>
|
|
||||||
<span className="label-text font-medium">{transI18n("useTechnique")}</span>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={avatars[avatarSelected?.ID.toString() || ""]?.techniques.length > 0 }
|
|
||||||
onChange={(e) => {
|
|
||||||
if (!avatarSelected || avatarSelected?.MazeBuff?.length == 0) return
|
|
||||||
const techniques = e.target.checked ? avatarSelected?.MazeBuff : [];
|
|
||||||
const newAvatar = { ...avatars[avatarSelected?.ID.toString() || ""], techniques: techniques };
|
|
||||||
setAvatar(newAvatar);
|
|
||||||
}}
|
|
||||||
className="toggle toggle-accent"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<div className="text-xs text-base-content/60 mt-1">
|
|
||||||
{transI18n("techniqueNote")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Enhancement Selection */}
|
|
||||||
{avatarSelected?.Enhanced && (
|
|
||||||
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10">
|
|
||||||
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
||||||
<svg className="w-5 h-5 text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 0 0 4.486-6.336l-3.276 3.277a3.004 3.004 0 0 1-2.25-2.25l3.276-3.276a4.5 4.5 0 0 0-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437 1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008Z" />
|
|
||||||
</svg>
|
|
||||||
{transI18n("enhancement")}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<div className="form-control">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-medium">{transI18n("enhancementLevel")}</span>
|
|
||||||
<span className="label-text-alt text-secondary font-mono">
|
|
||||||
{avatars[avatarSelected?.ID.toString() || ""]?.enhanced || transI18n("origin")}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={avatars[avatarSelected?.ID.toString() || ""]?.enhanced || ""}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newAvatar = avatars[avatarSelected?.ID.toString() || ""]
|
|
||||||
if (newAvatar) {
|
|
||||||
newAvatar.enhanced = e.target.value
|
|
||||||
const skillTree = getSkillTree(avatarSelected, e.target.value)
|
|
||||||
if (skillTree) {
|
|
||||||
newAvatar.data.skills = skillTree
|
|
||||||
}
|
|
||||||
setAvatar(newAvatar)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="select select-bordered select-secondary"
|
|
||||||
>
|
|
||||||
<option value="">{transI18n("origin")}</option>
|
|
||||||
{Object.keys(avatarSelected?.Enhanced || {}).map((key) => (
|
|
||||||
<option key={key} value={key}>
|
|
||||||
{key}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<div className="text-xs text-base-content/60 mt-1">
|
|
||||||
{transI18n("enhancedNote")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="divide-y divide-base-content/10">
|
||||||
|
{/* Level Control */}
|
||||||
|
<div className="py-4 space-y-2.5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-bold text-base-content tracking-tight flex items-center gap-1.5">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-primary" />
|
||||||
|
{transI18n("characterLevel")}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-mono font-bold px-2.5 py-1 rounded-md bg-primary/15 text-primary border border-primary/25">
|
||||||
|
{avatars[avatarSelected?.ID.toString() || ""]?.level} / 80
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 pt-0.5">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="80"
|
||||||
|
value={avatars[avatarSelected?.ID.toString() || ""]?.level}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newLevel = Math.min(80, Math.max(1, parseInt(e.target.value) || 1));
|
||||||
|
setAvatars({ ...avatars, [avatarSelected?.ID.toString() || ""]: { ...avatars[avatarSelected?.ID.toString() || ""], level: newLevel, promotion: calcPromotion(newLevel) } });
|
||||||
|
}}
|
||||||
|
className="range range-primary range-sm flex-1"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setAvatars({ ...avatars, [avatarSelected?.ID.toString() || ""]: { ...avatars[avatarSelected?.ID.toString() || ""], level: 80, promotion: calcPromotion(80) } });
|
||||||
|
}}
|
||||||
|
className="btn btn-sm btn-primary btn-outline h-8 min-h-8 rounded-lg font-bold px-3"
|
||||||
|
>
|
||||||
|
{transI18n("max")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Ultimate Energy Control */}
|
||||||
|
<div className="py-4 space-y-2.5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-bold text-base-content tracking-tight flex items-center gap-1.5">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-amber-400" />
|
||||||
|
{transI18n("ultimateEnergy")}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-mono font-bold text-amber-500">
|
||||||
|
{Math.round(avatars[avatarSelected?.ID.toString() || ""]?.sp_value)} / {avatars[avatarSelected?.ID.toString() || ""]?.sp_max}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-mono text-base-content/70 font-semibold">
|
||||||
|
({((avatars[avatarSelected?.ID.toString() || ""]?.sp_value / avatars[avatarSelected?.ID.toString() || ""]?.sp_max) * 100).toFixed(0)}%)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 pt-0.5">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max={avatars[avatarSelected?.ID.toString() || ""]?.sp_max}
|
||||||
|
value={avatars[avatarSelected?.ID.toString() || ""]?.sp_value}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!avatars[avatarSelected?.ID.toString() || ""]?.can_change_sp) return
|
||||||
|
const newSpValue = Math.min(avatars[avatarSelected?.ID.toString() || ""]?.sp_max, Math.max(0, parseInt(e.target.value) || 0));
|
||||||
|
setAvatars({ ...avatars, [avatarSelected?.ID.toString() || ""]: { ...avatars[avatarSelected?.ID.toString() || ""], sp_value: newSpValue } });
|
||||||
|
}}
|
||||||
|
className="range range-warning range-sm flex-1"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-warning btn-outline h-8 min-h-8 font-bold px-3 rounded-lg shadow-xs active-press"
|
||||||
|
onClick={() => {
|
||||||
|
if (!avatars[avatarSelected?.ID.toString() || ""]?.can_change_sp) return
|
||||||
|
const newSpValue = Math.ceil(avatars[avatarSelected?.ID.toString() || ""]?.sp_max / 2);
|
||||||
|
const newAvatar = { ...avatars[avatarSelected?.ID.toString() || ""], sp_value: newSpValue }
|
||||||
|
setAvatar(newAvatar)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{transI18n("setTo50")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Technique Toggle */}
|
||||||
|
<div className="py-4 flex items-center justify-between gap-4">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<span className="text-sm font-bold text-base-content tracking-tight flex items-center gap-1.5">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-secondary" />
|
||||||
|
{transI18n("useTechnique")}
|
||||||
|
</span>
|
||||||
|
<p className="text-xs text-base-content/70 font-medium pl-3">{transI18n("techniqueNote")}</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={avatars[avatarSelected?.ID.toString() || ""]?.techniques.length > 0}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!avatarSelected || avatarSelected?.MazeBuff?.length == 0) return
|
||||||
|
const techniques = e.target.checked ? avatarSelected?.MazeBuff : [];
|
||||||
|
const newAvatar = { ...avatars[avatarSelected?.ID.toString() || ""], techniques: techniques };
|
||||||
|
setAvatar(newAvatar);
|
||||||
|
}}
|
||||||
|
className="toggle toggle-primary"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Enhancement Selection */}
|
||||||
|
{avatarSelected?.Enhanced && (
|
||||||
|
<div className="py-4 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-bold text-base-content tracking-tight flex items-center gap-1.5">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-accent" />
|
||||||
|
{transI18n("enhancement")}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-mono text-secondary font-bold">
|
||||||
|
{avatars[avatarSelected?.ID.toString() || ""]?.enhanced || transI18n("origin")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
value={avatars[avatarSelected?.ID.toString() || ""]?.enhanced || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newAvatar = avatars[avatarSelected?.ID.toString() || ""]
|
||||||
|
if (newAvatar) {
|
||||||
|
newAvatar.enhanced = e.target.value
|
||||||
|
const skillTree = getSkillTree(avatarSelected, e.target.value)
|
||||||
|
if (skillTree) {
|
||||||
|
newAvatar.data.skills = skillTree
|
||||||
|
}
|
||||||
|
setAvatar(newAvatar)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="select select-sm select-bordered w-full rounded-xl bg-base-200/50"
|
||||||
|
>
|
||||||
|
<option value="">{transI18n("origin")}</option>
|
||||||
|
{Object.keys(avatarSelected?.Enhanced || {}).map((key) => (
|
||||||
|
<option key={key} value={key}>
|
||||||
|
{key}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<p className="text-xs text-base-content/60 font-medium">{transI18n("enhancedNote")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="m-2 min-h-96">
|
|
||||||
<div className="container">
|
{/* Right Column: Lightcone Equipment */}
|
||||||
<div className="card bg-base-200 shadow-xl">
|
<div className="glass-card rounded-2xl p-5 sm:p-6 border border-base-content/10 shadow-xs flex flex-col justify-between">
|
||||||
<div className="card-body">
|
<div>
|
||||||
{/* Header */}
|
{/* Card Header */}
|
||||||
<div className="flex items-center justify-between mb-6">
|
<div className="flex items-center justify-between pb-4 mb-2 border-b border-base-content/10">
|
||||||
<h2 className="card-title text-2xl font-bold flex items-center gap-2">
|
<h2 className="text-base sm:text-lg font-bold flex items-center gap-2.5">
|
||||||
<div className="w-2 h-8 bg-linear-to-b from-primary to-secondary rounded-full"></div>
|
<div className="w-1.5 h-5 bg-linear-to-b from-secondary to-primary rounded-full"></div>
|
||||||
{transI18n("lightconeEquipment")}
|
<span className="bg-clip-text text-transparent bg-linear-to-r from-secondary to-primary font-bold tracking-tight">
|
||||||
</h2>
|
{transI18n("lightconeEquipment")}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
{lightcone && (
|
||||||
|
<span className="text-xs font-mono font-semibold px-2.5 py-1 rounded-md bg-secondary/10 text-secondary border border-secondary/20">
|
||||||
|
S{lightcone.rank} • Lv.{lightcone.level}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{lightcone && lightconeDetail ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Lightcone Hero Block */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-12 gap-4 items-center">
|
||||||
|
{/* Artwork */}
|
||||||
|
<div className="sm:col-span-4 aspect-[3/4] rounded-xl overflow-hidden bg-base-200/60 flex items-center justify-center border border-base-content/10 shadow-xs">
|
||||||
|
<Image
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
width={904}
|
||||||
|
height={1260}
|
||||||
|
priority
|
||||||
|
src={`${process.env.CDN_URL}/${lightconeDetail?.Image?.ImagePath}`}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
alt="Lightcone"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Meta & Stats */}
|
||||||
|
<div className="sm:col-span-8 space-y-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base sm:text-lg font-bold tracking-tight text-base-content">
|
||||||
|
<ParseText
|
||||||
|
locale={locale}
|
||||||
|
text={getLocaleName(locale, lightconeDetail.Name)}
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
|
||||||
|
<span className="text-xs px-2.5 py-0.5 rounded-md bg-base-200 font-semibold text-base-content/80 border border-base-content/10">
|
||||||
|
{transI18n(lightconeDetail.BaseType.toLowerCase())}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs px-2.5 py-0.5 rounded-md bg-amber-500/15 text-amber-500 font-bold border border-amber-500/25">
|
||||||
|
{calcRarity(lightconeDetail.Rarity)}★
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-mono text-base-content/60 font-medium">#{lightcone.item_id}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Level Slider */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center justify-between text-xs font-semibold">
|
||||||
|
<span className="text-base-content/80">{transI18n("level")}</span>
|
||||||
|
<span className="font-mono text-primary font-bold">{lightcone.level}/80</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="1"
|
||||||
|
max="80"
|
||||||
|
value={lightcone.level || 1}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newLightconeLevel = Math.min(80, Math.max(1, parseInt(e.target.value) || 1))
|
||||||
|
handleUpdateLightcone((lc) => lc ? { ...lc, level: newLightconeLevel, promotion: calcPromotion(newLightconeLevel) } : null)
|
||||||
|
}}
|
||||||
|
className="range range-primary range-sm w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Superimposition Rank (S1-S5) */}
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center justify-between text-xs font-semibold">
|
||||||
|
<span className="text-base-content/80">{transI18n("superimpositionRank")}</span>
|
||||||
|
<span className="font-mono text-info font-bold">S{lightcone.rank}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-5 gap-1.5">
|
||||||
|
{[1, 2, 3, 4, 5].map((r) => (
|
||||||
|
<button
|
||||||
|
key={r}
|
||||||
|
onClick={() => {
|
||||||
|
handleUpdateLightcone((lc) => lc ? { ...lc, rank: r } : null)
|
||||||
|
}}
|
||||||
|
className={`btn btn-xs rounded-lg font-mono font-bold transition-all ${
|
||||||
|
lightcone.rank === r
|
||||||
|
? 'btn-primary shadow-xs'
|
||||||
|
: 'btn-ghost bg-base-200 hover:bg-base-300 text-base-content/80'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
S{r}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{lightcone && lightconeDetail ? (
|
{/* Skill Description */}
|
||||||
<div className="grid lg:grid-cols-3 gap-6">
|
<div className="p-3.5 rounded-xl bg-base-200/50 border border-base-content/10">
|
||||||
<div className="lg:col-span-3">
|
<div
|
||||||
{/* Level & Rank Controls */}
|
className="text-xs sm:text-sm text-base-content/90 leading-relaxed font-normal"
|
||||||
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10">
|
dangerouslySetInnerHTML={{
|
||||||
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
__html: replaceByParam(
|
||||||
<div className="w-2 h-6 bg-linear-to-b from-accent to-accent/50 rounded-full"></div>
|
getLocaleName(locale, lightconeDetail.Skills.Desc),
|
||||||
{transI18n("lightconeSettings")}
|
lightconeDetail.Skills.Level[lightcone.rank.toString()]?.Param || []
|
||||||
</h4>
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-6">
|
{/* Action Buttons */}
|
||||||
{/* Level Input */}
|
<div className="flex items-center gap-3 pt-2">
|
||||||
<div className="form-control">
|
<button
|
||||||
<label className="label">
|
onClick={() => {
|
||||||
<span className="label-text font-medium">{transI18n("level")}</span>
|
if (avatarSelected) {
|
||||||
<span className="label-text-alt text-primary font-mono">{lightcone.level}/80</span>
|
setResetDataLightcone(avatarSelected, baseType)
|
||||||
</label>
|
handleShow("action_detail_modal")
|
||||||
<div className="relative">
|
}
|
||||||
<input
|
}}
|
||||||
type="number"
|
className="btn btn-primary btn-sm flex-1 rounded-xl font-bold shadow-xs"
|
||||||
min="1"
|
>
|
||||||
max="80"
|
{transI18n("changeLightcone")}
|
||||||
value={lightcone.level || 1}
|
</button>
|
||||||
onChange={(e) => {
|
<button
|
||||||
const newLightconeLevel = Math.min(80, Math.max(1, parseInt(e.target.value) || 1))
|
onClick={() => {
|
||||||
const newLightcone = { ...lightcone, level: newLightconeLevel, promotion: calcPromotion(newLightconeLevel) }
|
handleUpdateLightcone(() => null)
|
||||||
const newAvatar = { ...avatars[avatarSelected.ID] }
|
}}
|
||||||
newAvatar.profileList[newAvatar.profileSelect].lightcone = newLightcone
|
className="btn btn-error btn-outline hover:btn-error btn-sm rounded-xl font-bold px-3.5 gap-1.5 transition-all"
|
||||||
setAvatar(newAvatar)
|
title={transI18n("removeLightcone")}
|
||||||
}}
|
>
|
||||||
className="input input-bordered w-full pr-16 font-mono"
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
placeholder={transI18n("placeholderLevel")}
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||||
/>
|
</svg>
|
||||||
<div
|
<span className="text-xs">{transI18n("removeLightcone")}</span>
|
||||||
onClick={() => {
|
</button>
|
||||||
const newLightcone = { ...lightcone, level: 80, promotion: calcPromotion(80) }
|
</div>
|
||||||
const newAvatar = { ...avatars[avatarSelected.ID] }
|
|
||||||
newAvatar.profileList[newAvatar.profileSelect].lightcone = newLightcone
|
|
||||||
setAvatar(newAvatar)
|
|
||||||
}}
|
|
||||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-base-content/60 cursor-pointer">
|
|
||||||
<span className="text-sm">{transI18n("max")}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="1"
|
|
||||||
max="80"
|
|
||||||
value={lightcone.level || 1}
|
|
||||||
onChange={(e) => {
|
|
||||||
const newLightconeLevel = Math.min(80, Math.max(1, parseInt(e.target.value) || 1))
|
|
||||||
const newLightcone = { ...lightcone, level: newLightconeLevel, promotion: calcPromotion(newLightconeLevel) }
|
|
||||||
const newAvatar = { ...avatars[avatarSelected.ID] }
|
|
||||||
newAvatar.profileList[newAvatar.profileSelect].lightcone = newLightcone
|
|
||||||
setAvatar(newAvatar)
|
|
||||||
}}
|
|
||||||
className="range range-primary range-sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Rank Selection */}
|
|
||||||
<div className="form-control space-y-3">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
|
||||||
<span className="font-medium text-sm sm:text-base">{transI18n("superimpositionRank")}</span>
|
|
||||||
<span className="text-info font-mono text-lg sm:text-xl font-semibold">S{lightcone.rank}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Rank Buttons */}
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="grid grid-cols-5 gap-1.5 sm:gap-2 md:grid-cols-3 xl:grid-cols-5 max-w-sm mx-auto sm:mx-0">
|
|
||||||
{[1, 2, 3, 4, 5].map((r) => (
|
|
||||||
<button
|
|
||||||
key={r}
|
|
||||||
onClick={() => {
|
|
||||||
const newLightcone = { ...lightcone, rank: r }
|
|
||||||
const newAvatar = { ...avatars[avatarSelected.ID] }
|
|
||||||
newAvatar.profileList[newAvatar.profileSelect].lightcone = newLightcone
|
|
||||||
setAvatar(newAvatar)
|
|
||||||
}}
|
|
||||||
className={`
|
|
||||||
btn btn-sm sm:btn-md font-mono font-semibold
|
|
||||||
transition-all duration-200 hover:scale-105
|
|
||||||
${lightcone.rank === r
|
|
||||||
? 'btn-primary shadow-lg'
|
|
||||||
: 'btn-outline btn-primary hover:btn-primary'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
S{r}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Help Text */}
|
|
||||||
<div className="text-xs sm:text-sm text-base-content/60 text-center sm:text-left mt-2">
|
|
||||||
{transI18n("ranksNote")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Action Buttons */}
|
|
||||||
<div className="flex flex-wrap gap-3 mt-2">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (avatarSelected) {
|
|
||||||
setResetDataLightcone(avatarSelected, baseType)
|
|
||||||
handleShow("action_detail_modal")
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="btn btn-primary btn-lg flex-1 min-w-fit"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
|
||||||
</svg>
|
|
||||||
{transI18n("changeLightcone")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
const newAvatar = { ...avatars[avatarSelected.ID] }
|
|
||||||
newAvatar.profileList[newAvatar.profileSelect].lightcone = null
|
|
||||||
setAvatar(newAvatar)
|
|
||||||
}}
|
|
||||||
className="btn btn-warning btn-lg flex-1 min-w-fit"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
|
||||||
</svg>
|
|
||||||
{transI18n("removeLightcone")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Lightcone Image */}
|
|
||||||
<div className="lg:col-span-1">
|
|
||||||
<div className="">
|
|
||||||
<Image
|
|
||||||
unoptimized
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
width={904}
|
|
||||||
height={1260}
|
|
||||||
priority
|
|
||||||
src={`${process.env.CDN_URL}/${lightconeDetail?.Image?.ImagePath}`}
|
|
||||||
className="w-full h-full rounded-lg object-cover shadow-lg"
|
|
||||||
alt="Lightcone"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Lightcone Info & Controls */}
|
|
||||||
<div className="lg:col-span-2 space-y-6">
|
|
||||||
{/* Basic Info */}
|
|
||||||
{lightconeDetail && (
|
|
||||||
<div className="bg-base-300 rounded-xl p-6 border border-base-content/10">
|
|
||||||
<div className="flex flex-wrap items-center gap-3 mb-4">
|
|
||||||
<h3 className="text-2xl font-bold text-base-content">
|
|
||||||
<ParseText
|
|
||||||
locale={locale}
|
|
||||||
text={getLocaleName(locale, lightconeDetail.Name)}
|
|
||||||
/>
|
|
||||||
</h3>
|
|
||||||
<div className="badge badge-outline badge-lg">
|
|
||||||
{transI18n(lightconeDetail.BaseType.toLowerCase())}
|
|
||||||
</div>
|
|
||||||
<div className="badge badge-outline badge-lg">
|
|
||||||
{calcRarity(lightconeDetail.Rarity) + "⭐"}
|
|
||||||
</div>
|
|
||||||
<div className="badge badge-outline badge-lg">
|
|
||||||
{"id: " + lightcone.item_id}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="prose prose-sm max-w-none">
|
|
||||||
<div
|
|
||||||
className="text-base-content/80 leading-relaxed"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: replaceByParam(
|
|
||||||
getLocaleName(locale, lightconeDetail.Skills.Desc),
|
|
||||||
lightconeDetail.Skills.Level[lightcone.rank.toString()]?.Param || []
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
/* No Lightcone Equipped State */
|
|
||||||
<div className="text-center py-12">
|
|
||||||
<div
|
|
||||||
onClick={() => handleShow("action_detail_modal")}
|
|
||||||
className="w-24 h-24 mx-auto mb-6 bg-base-300 rounded-full flex items-center justify-center cursor-pointer"
|
|
||||||
>
|
|
||||||
<svg className="w-12 h-12 text-base-content/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h3 className="text-xl font-semibold mb-2">{transI18n("noLightconeEquipped")}</h3>
|
|
||||||
<p className="text-base-content/60 mb-6">{transI18n("equipLightconeNote")}</p>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (avatarSelected) {
|
|
||||||
setResetDataLightcone(avatarSelected, baseType)
|
|
||||||
handleShow("action_detail_modal")
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="btn btn-success btn-lg"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
|
||||||
</svg>
|
|
||||||
{transI18n("equipLightcone")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
/* Empty Lightcone State */
|
||||||
|
<div className="text-center py-8 space-y-3.5">
|
||||||
|
<div
|
||||||
|
onClick={() => handleShow("action_detail_modal")}
|
||||||
|
className="w-16 h-16 mx-auto bg-base-200/70 rounded-2xl border-2 border-dashed border-base-content/30 flex items-center justify-center cursor-pointer hover:border-primary hover:scale-105 transition-all group"
|
||||||
|
>
|
||||||
|
<svg className="w-7 h-7 text-base-content/50 group-hover:text-primary transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-bold text-base-content">{transI18n("noLightconeEquipped")}</h3>
|
||||||
|
<p className="text-xs text-base-content/60 font-medium mt-0.5">{transI18n("equipLightconeNote")}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
if (avatarSelected) {
|
||||||
|
setResetDataLightcone(avatarSelected, baseType)
|
||||||
|
handleShow("action_detail_modal")
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="btn btn-primary btn-sm rounded-xl font-bold px-6 shadow-xs"
|
||||||
|
>
|
||||||
|
{transI18n("equipLightcone")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<dialog id="action_detail_modal" className="modal backdrop-blur-sm">
|
{/* Lightcone Selection Modal */}
|
||||||
<div className="modal-box w-11/12 max-w-5xl max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
|
<dialog id="action_detail_modal" className="modal">
|
||||||
<div className="sticky top-0 z-10">
|
<div className="modal-box w-11/12 max-w-[92vw] sm:max-w-[88vw] max-h-[90vh] bg-base-100 text-base-content border border-white/10 shadow-2xl rounded-2xl p-0 overflow-hidden">
|
||||||
<motion.button
|
<div className="sticky top-0 z-20 flex items-center justify-between px-6 py-4 border-b border-base-content/10 bg-base-100">
|
||||||
whileHover={{ scale: 1.1, rotate: 90 }}
|
<h3 className="font-bold text-xl tracking-tight bg-clip-text text-transparent bg-linear-to-r from-primary via-accent to-secondary">
|
||||||
transition={{ duration: 0.2 }}
|
{transI18n("lightconeEquipment")}
|
||||||
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
|
</h3>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-circle btn-ghost text-base-content/60 hover:text-base-content hover:bg-base-200 transition-colors"
|
||||||
onClick={() => handleCloseModal("action_detail_modal")}
|
onClick={() => handleCloseModal("action_detail_modal")}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</motion.button>
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 sm:p-6 overflow-y-auto max-h-[calc(90vh-5rem)]">
|
||||||
|
<LightconeBar />
|
||||||
</div>
|
</div>
|
||||||
<LightconeBar />
|
|
||||||
</div>
|
</div>
|
||||||
|
<form method="dialog" className="modal-backdrop">
|
||||||
|
<button onClick={() => handleCloseModal("action_detail_modal")}>close</button>
|
||||||
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,78 +6,111 @@ import ParseText from '../parseText';
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import useDetailDataStore from '@/stores/detailDataStore';
|
import useDetailDataStore from '@/stores/detailDataStore';
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
|
import useUserDataStore from '@/stores/userDataStore';
|
||||||
|
|
||||||
interface CharacterCardProps {
|
interface CharacterCardProps {
|
||||||
data: AvatarDetail
|
data: AvatarDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CharacterCard({ data }: CharacterCardProps) {
|
export default function CharacterCard({ data }: CharacterCardProps) {
|
||||||
const { locale } = useLocaleStore();
|
const { locale } = useLocaleStore(useShallow((state) => ({ locale: state.locale })));
|
||||||
const transI18n = useTranslations("DataPage");
|
const transI18n = useTranslations("DataPage");
|
||||||
const { baseType, damageType } = useDetailDataStore()
|
const { baseType, damageType } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
baseType: state.baseType,
|
||||||
|
damageType: state.damageType,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { avatars } = useUserDataStore(
|
||||||
|
useShallow((state) => ({ avatars: state.avatars }))
|
||||||
|
)
|
||||||
|
|
||||||
|
const userAvatar = avatars[data.ID.toString()]
|
||||||
|
const level = userAvatar?.level
|
||||||
|
const rank = userAvatar?.data?.rank ?? 0
|
||||||
|
|
||||||
|
const is5Star = data.Rarity === "CombatPowerAvatarRarityType5";
|
||||||
|
const is4Star = data.Rarity === "CombatPowerAvatarRarityType4";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
className="z-10 flex flex-col items-center rounded-xl shadow-xl
|
className="group relative flex flex-col items-center rounded-xl overflow-hidden bg-base-100 border border-base-content/10 shadow-sm hover:shadow-md cursor-pointer select-none h-full"
|
||||||
bg-linear-to-br from-base-300 via-base-100 to-warning/70
|
|
||||||
transform transition-transform duration-300 ease-in-out
|
|
||||||
hover:scale-105 cursor-pointer min-h-45 sm:min-h-45 md:min-h-52.5 lg:min-h-55 xl:min-h-60 2xl:min-h-65"
|
|
||||||
>
|
>
|
||||||
|
{/* Artwork container with vibrant rarity background */}
|
||||||
<div
|
<div
|
||||||
className={`w-full rounded-md bg-linear-to-br ${data.Rarity === "CombatPowerAvatarRarityType5"
|
className={`w-full relative aspect-[3/4] flex items-center justify-center overflow-hidden bg-linear-to-b ${
|
||||||
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
|
is5Star
|
||||||
: "from-purple-400 via-purple-600/70 to-purple-800/50"
|
? "from-yellow-400 via-amber-500/85 to-amber-700/70"
|
||||||
}`}
|
: is4Star
|
||||||
|
? "from-purple-400 via-purple-600/80 to-purple-800/60"
|
||||||
|
: "from-sky-400 via-blue-600/80 to-blue-800/60"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
|
<Image
|
||||||
|
width={376}
|
||||||
|
height={512}
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src={`${process.env.CDN_URL}/${data.Image.AvatarIconPath}`}
|
||||||
|
priority={true}
|
||||||
|
className="w-full h-full object-contain transition-transform duration-300 ease-out group-hover:scale-105"
|
||||||
|
alt={data.ID.toString()}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="relative w-full h-32 lg:h-26 xl:h-36">
|
{/* Level and Eidolons Badge Overlay */}
|
||||||
|
{level && (
|
||||||
|
<div className="absolute bottom-1 left-1 flex items-center gap-1 z-10">
|
||||||
|
<span className="bg-black/60 backdrop-blur-xs text-white text-[10px] font-bold font-mono px-1.5 py-0.2 rounded shadow-xs">
|
||||||
|
{level}
|
||||||
|
</span>
|
||||||
|
{rank > 0 && (
|
||||||
|
<span className="bg-amber-500/80 text-black text-[10px] font-black font-mono px-1 py-0.2 rounded shadow-xs">
|
||||||
|
E{rank}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Damage Type Badge */}
|
||||||
|
{damageType?.[data.DamageType]?.Icon && (
|
||||||
<Image
|
<Image
|
||||||
width={376}
|
width={28}
|
||||||
height={512}
|
height={28}
|
||||||
unoptimized
|
unoptimized
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
src={`${process.env.CDN_URL}/${data.Image.AvatarIconPath}`}
|
src={`${process.env.CDN_URL}/${damageType[data.DamageType].Icon}`}
|
||||||
priority={true}
|
className="absolute top-1 left-1 w-6 h-6 object-contain drop-shadow-sm"
|
||||||
className="rounded-md w-full h-full object-contain"
|
|
||||||
alt="ALT"
|
|
||||||
/>
|
|
||||||
<Image
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
unoptimized
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
src={`${process.env.CDN_URL}/${damageType?.[data.DamageType].Icon}`}
|
|
||||||
className="absolute top-0 left-0 w-6 h-6 rounded-full"
|
|
||||||
alt={data.DamageType.toLowerCase()}
|
alt={data.DamageType.toLowerCase()}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Base Type / Path Badge */}
|
||||||
|
{baseType?.[data.BaseType]?.Icon && (
|
||||||
<Image
|
<Image
|
||||||
width={32}
|
width={28}
|
||||||
height={32}
|
height={28}
|
||||||
unoptimized
|
unoptimized
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
src={`${process.env.CDN_URL}/${baseType?.[data.BaseType].Icon}`}
|
src={`${process.env.CDN_URL}/${baseType[data.BaseType].Icon}`}
|
||||||
className="absolute top-0 right-0 w-6 h-6 rounded-full"
|
className="absolute top-1 right-1 w-6 h-6 object-contain rounded-full drop-shadow-sm"
|
||||||
alt={data.BaseType.toLowerCase()}
|
alt={data.BaseType.toLowerCase()}
|
||||||
style={{
|
style={{
|
||||||
boxShadow: "inset 0 0 8px 4px #9CA3AF"
|
boxShadow: "inset 0 0 6px 3px rgba(255, 255, 255, 0.4)"
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ParseText
|
{/* Name Label - Clear Full Name Display */}
|
||||||
locale={locale}
|
<div className="w-full py-1.5 px-1 text-center flex items-center justify-center bg-base-100 min-h-11">
|
||||||
text={getNameChar(locale, transI18n, data)}
|
<ParseText
|
||||||
className="
|
locale={locale}
|
||||||
w-full px-0.5
|
text={getNameChar(locale, transI18n, data)}
|
||||||
my-1
|
className="font-bold text-xs sm:text-[11.5px] lg:text-xs leading-tight px-0.5 text-base-content group-hover:text-primary transition-colors text-center w-full"
|
||||||
text-center font-bold text-shadow-white
|
/>
|
||||||
leading-tight
|
</div>
|
||||||
wrap-break-word
|
|
||||||
text-sm sm:text-base 2xl:text-lg
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,84 +7,118 @@ import Image from 'next/image';
|
|||||||
import ParseText from '../parseText';
|
import ParseText from '../parseText';
|
||||||
import useDetailDataStore from '@/stores/detailDataStore';
|
import useDetailDataStore from '@/stores/detailDataStore';
|
||||||
import { getLocaleName } from '@/helper/getName';
|
import { getLocaleName } from '@/helper/getName';
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
export default function CharacterInfoCard({ character, selectedCharacters, onCharacterToggle }: { character: CharacterInfoCardType, selectedCharacters: CharacterInfoCardType[], onCharacterToggle: (characterId: CharacterInfoCardType) => void }) {
|
export default function CharacterInfoCard({
|
||||||
|
character,
|
||||||
|
selectedCharacters,
|
||||||
|
onCharacterToggle
|
||||||
|
}: {
|
||||||
|
character: CharacterInfoCardType,
|
||||||
|
selectedCharacters: CharacterInfoCardType[],
|
||||||
|
onCharacterToggle: (characterId: CharacterInfoCardType) => void
|
||||||
|
}) {
|
||||||
const isSelected = selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id);
|
const isSelected = selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id);
|
||||||
const { mapAvatar, mapLightCone, baseType, damageType } = useDetailDataStore();
|
const { mapAvatar, mapLightCone, baseType, damageType } = useDetailDataStore(
|
||||||
const { locale } = useLocaleStore();
|
useShallow((state) => ({
|
||||||
|
mapAvatar: state.mapAvatar,
|
||||||
|
mapLightCone: state.mapLightCone,
|
||||||
|
baseType: state.baseType,
|
||||||
|
damageType: state.damageType,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const { locale } = useLocaleStore(
|
||||||
|
useShallow((state) => ({ locale: state.locale }))
|
||||||
|
);
|
||||||
|
|
||||||
|
const avatar = mapAvatar?.[character.avatar_id.toString()];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`bg-base-200/60 rounded-xl p-4 border cursor-pointer transition-all duration-200 ${isSelected
|
className={`group rounded-2xl p-4 border transition-all duration-200 cursor-pointer select-none ${
|
||||||
? 'border-blue-400 ring-2 ring-blue-400/50'
|
isSelected
|
||||||
: 'border-base-300/50 hover:border-base-300 opacity-75'
|
? 'bg-primary/10 border-primary shadow-lg shadow-primary/20 ring-2 ring-primary/40'
|
||||||
}`}
|
: 'bg-base-200/60 border-white/5 hover:border-white/20 opacity-80 hover:opacity-100 hover:bg-base-200'
|
||||||
|
}`}
|
||||||
onClick={() => onCharacterToggle(character)}
|
onClick={() => onCharacterToggle(character)}
|
||||||
>
|
>
|
||||||
|
|
||||||
{/* Character Portrait */}
|
{/* Character Portrait */}
|
||||||
<div className="relative mb-4">
|
<div className="relative mb-3 aspect-[4/5] rounded-xl overflow-hidden bg-base-300/40 flex items-center justify-center">
|
||||||
<div className="w-full h-48 rounded-lg overflow-hidden relative">
|
{avatar && (
|
||||||
<Image
|
<>
|
||||||
src={`${process.env.CDN_URL}/${mapAvatar?.[character.avatar_id.toString()]?.Image?.AvatarIconPath}`}
|
<Image
|
||||||
alt={getLocaleName(locale, mapAvatar?.[character.avatar_id.toString()]?.Name)}
|
src={`${process.env.CDN_URL}/${avatar.Image?.AvatarIconPath}`}
|
||||||
width={376}
|
alt={getLocaleName(locale, avatar.Name)}
|
||||||
height={512}
|
width={376}
|
||||||
unoptimized
|
height={512}
|
||||||
crossOrigin="anonymous"
|
unoptimized
|
||||||
className="w-full h-full object-contain"
|
crossOrigin="anonymous"
|
||||||
/>
|
className="w-full h-full object-contain transition-transform duration-300 group-hover:scale-105"
|
||||||
<Image
|
/>
|
||||||
width={48}
|
{damageType?.[avatar.DamageType]?.Icon && (
|
||||||
height={48}
|
<div className="absolute top-1.5 left-1.5 p-0.5 rounded-full bg-black/60 backdrop-blur-xs border border-white/10">
|
||||||
unoptimized
|
<Image
|
||||||
crossOrigin="anonymous"
|
width={24}
|
||||||
src={`${process.env.CDN_URL}/${damageType?.[mapAvatar?.[character.avatar_id.toString()]?.DamageType || ""]?.Icon}`}
|
height={24}
|
||||||
className="absolute top-0 left-0 w-10 h-10 rounded-full"
|
unoptimized
|
||||||
alt={mapAvatar[character.avatar_id.toString()]?.DamageType.toLowerCase()}
|
crossOrigin="anonymous"
|
||||||
/>
|
src={`${process.env.CDN_URL}/${damageType[avatar.DamageType].Icon}`}
|
||||||
<Image
|
className="w-5 h-5 object-contain"
|
||||||
width={48}
|
alt={avatar.DamageType.toLowerCase()}
|
||||||
height={48}
|
/>
|
||||||
unoptimized
|
</div>
|
||||||
crossOrigin="anonymous"
|
)}
|
||||||
src={`${process.env.CDN_URL}/${baseType?.[mapAvatar?.[character.avatar_id.toString()]?.BaseType || ""]?.Icon}`}
|
{baseType?.[avatar.BaseType]?.Icon && (
|
||||||
className="absolute top-0 right-0 w-10 h-10 rounded-full"
|
<div className="absolute top-1.5 right-1.5 p-0.5 rounded-full bg-black/60 backdrop-blur-xs border border-white/10">
|
||||||
alt={mapAvatar[character.avatar_id.toString()]?.BaseType.toLowerCase()}
|
<Image
|
||||||
style={{
|
width={24}
|
||||||
boxShadow: "inset 0 0 8px 4px #9CA3AF"
|
height={24}
|
||||||
}}
|
unoptimized
|
||||||
/>
|
crossOrigin="anonymous"
|
||||||
|
src={`${process.env.CDN_URL}/${baseType[avatar.BaseType].Icon}`}
|
||||||
|
className="w-5 h-5 object-contain"
|
||||||
|
alt={avatar.BaseType.toLowerCase()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Character Name and Level Badge */}
|
||||||
|
<div className="text-center mb-3">
|
||||||
|
<ParseText
|
||||||
|
className="text-base font-bold line-clamp-1"
|
||||||
|
text={avatar ? getLocaleName(locale, avatar.Name) : character.avatar_id.toString()}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center justify-center gap-2 mt-1">
|
||||||
|
<span className="text-xs font-mono font-semibold px-2 py-0.5 rounded-full bg-base-300/80 text-base-content/80">
|
||||||
|
Lv.{character.level}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs font-mono font-bold px-2 py-0.5 rounded-full bg-amber-500/20 text-amber-400 border border-amber-500/30">
|
||||||
|
E{character.rank}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Character Name and Level */}
|
{/* Relics Mini-grid */}
|
||||||
<div className="w-full rounded-lg flex items-center justify-center mb-2">
|
|
||||||
<div className="text-center">
|
|
||||||
<ParseText className="text-lg font-bold"
|
|
||||||
text={getLocaleName(locale, mapAvatar[character.avatar_id.toString()]?.Name)}
|
|
||||||
locale={locale}
|
|
||||||
/>
|
|
||||||
<div className="text-base mb-1">Lv.{character.level} E{character.rank}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{character.relics.length > 0 && (
|
{character.relics.length > 0 && (
|
||||||
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
|
<div className="flex flex-wrap items-center justify-center gap-1.5 mb-3 p-1.5 rounded-xl bg-base-100/50 border border-white/5">
|
||||||
{character.relics.map((relic, index) => (
|
{character.relics.map((relic, index) => (
|
||||||
<div key={index} className="relative">
|
<div key={index} className="relative group/relic">
|
||||||
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50">
|
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-base-200 border border-amber-500/40">
|
||||||
<Image
|
<Image
|
||||||
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
|
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
|
||||||
alt="Relic"
|
alt="Relic"
|
||||||
unoptimized
|
unoptimized
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
width={124}
|
width={32}
|
||||||
height={124}
|
height={32}
|
||||||
className="w-14 h-14 object-contain"
|
className="w-6 h-6 object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white text-xs px-1 rounded">
|
<div className="absolute -bottom-1.5 left-1/2 -translate-x-1/2 bg-black/80 text-[9px] font-mono font-bold text-amber-400 px-1 rounded-full border border-white/10">
|
||||||
+{relic.level}
|
+{relic.level}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,30 +127,28 @@ export default function CharacterInfoCard({ character, selectedCharacters, onCha
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Light Cone */}
|
{/* Light Cone */}
|
||||||
{character.lightcone.item_id && (
|
{character.lightcone && character.lightcone.item_id !== 0 && (
|
||||||
<div className="">
|
<div className="p-2 rounded-xl bg-base-100/50 border border-white/5 text-center">
|
||||||
<div className="rounded-lg h-42 flex items-center justify-center">
|
<div className="h-28 flex items-center justify-center mb-1">
|
||||||
<Image
|
<Image
|
||||||
unoptimized
|
unoptimized
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
src={`${process.env.CDN_URL}/${mapLightCone?.[character.lightcone.item_id.toString()]?.Image?.ImagePath}`}
|
src={`${process.env.CDN_URL}/${mapLightCone?.[character.lightcone.item_id.toString()]?.Image?.ImagePath}`}
|
||||||
alt={getLocaleName(locale, mapLightCone?.[character.lightcone.item_id.toString()]?.Name)}
|
alt="Lightcone"
|
||||||
width={348}
|
width={160}
|
||||||
height={408}
|
height={200}
|
||||||
className="w-full h-full object-contain rounded-lg"
|
className="h-full w-auto object-contain rounded-lg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full rounded-lg flex items-center justify-center">
|
<div className="text-xs font-bold truncate">
|
||||||
<div className="text-center">
|
<ParseText
|
||||||
<div className="text-lg font-bold">
|
text={getLocaleName(locale, mapLightCone?.[character.lightcone.item_id.toString()]?.Name)}
|
||||||
<ParseText
|
locale={locale}
|
||||||
text={getLocaleName(locale, mapLightCone[character.lightcone.item_id.toString()]?.Name)}
|
/>
|
||||||
locale={locale}
|
</div>
|
||||||
/>
|
<div className="flex items-center justify-center gap-2 mt-1">
|
||||||
</div>
|
<span className="text-[11px] font-mono text-base-content/70">Lv.{character.lightcone.level}</span>
|
||||||
<div className="text-base mb-1">Lv.{character.lightcone.level} S{character.lightcone.rank}</div>
|
<span className="text-[11px] font-mono font-bold text-primary">S{character.lightcone.rank}</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { getLocaleName } from '@/helper';
|
import { getLocaleName } from '@/helper';
|
||||||
import useLocaleStore from '@/stores/localeStore';
|
import useLocaleStore from '@/stores/localeStore';
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
import ParseText from '../parseText';
|
import ParseText from '../parseText';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { LightConeDetail } from '@/types';
|
import { LightConeDetail } from '@/types';
|
||||||
@@ -11,43 +12,44 @@ interface LightconeCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function LightconeCard({ data }: LightconeCardProps) {
|
export default function LightconeCard({ data }: LightconeCardProps) {
|
||||||
|
const { locale } = useLocaleStore(useShallow((state) => ({ locale: state.locale })));
|
||||||
|
const text = getLocaleName(locale, data.Name);
|
||||||
|
|
||||||
|
const is5Star = data.Rarity === "CombatPowerLightconeRarity5";
|
||||||
|
const is4Star = data.Rarity === "CombatPowerLightconeRarity4";
|
||||||
|
|
||||||
const { locale } = useLocaleStore();
|
|
||||||
const text = getLocaleName(locale, data.Name)
|
|
||||||
return (
|
return (
|
||||||
<li className="z-10 flex flex-col items-center rounded-md shadow-lg
|
<li className="group relative flex flex-col items-center rounded-xl overflow-hidden bg-base-100 border border-base-content/10 shadow-sm hover:shadow-md cursor-pointer select-none h-full">
|
||||||
bg-linear-to-b from-customStart to-customEnd transform transition-transform duration-300
|
{/* Artwork container with vibrant rarity background */}
|
||||||
hover:scale-105 cursor-pointer min-h-55"
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={`w-full rounded-md bg-linear-to-br ${data.Rarity === "CombatPowerLightconeRarity5"
|
className={`w-full relative aspect-[3/4] flex items-center justify-center overflow-hidden bg-linear-to-b ${
|
||||||
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
|
is5Star
|
||||||
: data.Rarity === "CombatPowerLightconeRarity4" ? "from-purple-400 via-purple-600/70 to-purple-800/50" :
|
? "from-yellow-400 via-amber-500/85 to-amber-700/70"
|
||||||
"from-blue-400 via-blue-600/70 to-blue-800/50"
|
: is4Star
|
||||||
}`}
|
? "from-purple-400 via-purple-600/80 to-purple-800/60"
|
||||||
|
: "from-sky-400 via-blue-600/80 to-blue-800/60"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
|
<Image
|
||||||
<div className="relative w-full h-full">
|
loading="lazy"
|
||||||
<Image
|
src={`${process.env.CDN_URL}/${data?.Image?.ThumbnailPath}`}
|
||||||
loading="lazy"
|
unoptimized
|
||||||
src={`${process.env.CDN_URL}/${data?.Image?.ThumbnailPath}`}
|
crossOrigin="anonymous"
|
||||||
unoptimized
|
width={348}
|
||||||
crossOrigin="anonymous"
|
height={408}
|
||||||
width={348}
|
className="w-full h-full object-cover transition-transform duration-300 ease-out group-hover:scale-105"
|
||||||
height={408}
|
alt={data?.Name?.en || "Lightcone"}
|
||||||
className="w-full h-full rounded-md object-cover"
|
/>
|
||||||
alt="ALT"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ParseText
|
{/* Name Label - Uniform Fixed-Height Footer */}
|
||||||
locale={locale}
|
<div className="w-full h-11 px-1 py-0.5 text-center flex items-center justify-center bg-base-100">
|
||||||
text={text}
|
<ParseText
|
||||||
className="mt-2 px-1 text-center text-sm sm:text-base font-bold leading-tight"
|
locale={locale}
|
||||||
/>
|
text={text}
|
||||||
|
className="font-bold text-xs leading-tight line-clamp-2 text-base-content group-hover:text-primary transition-colors text-center"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,67 +7,78 @@ import Image from 'next/image';
|
|||||||
import ParseText from '../parseText';
|
import ParseText from '../parseText';
|
||||||
import useDetailDataStore from '@/stores/detailDataStore';
|
import useDetailDataStore from '@/stores/detailDataStore';
|
||||||
import { getLocaleName } from '@/helper/getName';
|
import { getLocaleName } from '@/helper/getName';
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
|
export default function ProfileCard({
|
||||||
export default function ProfileCard({ profile, selectedProfile, onProfileToggle }: { profile: AvatarProfileCardType, selectedProfile: AvatarProfileCardType[], onProfileToggle: (profileId: AvatarProfileCardType) => void }) {
|
profile,
|
||||||
|
selectedProfile,
|
||||||
|
onProfileToggle
|
||||||
|
}: {
|
||||||
|
profile: AvatarProfileCardType,
|
||||||
|
selectedProfile: AvatarProfileCardType[],
|
||||||
|
onProfileToggle: (profileId: AvatarProfileCardType) => void
|
||||||
|
}) {
|
||||||
const isSelected = selectedProfile.some((selectedProfile) => selectedProfile.key === profile.key);
|
const isSelected = selectedProfile.some((selectedProfile) => selectedProfile.key === profile.key);
|
||||||
const { mapLightCone } = useDetailDataStore();
|
const { mapLightCone } = useDetailDataStore(
|
||||||
const { locale } = useLocaleStore();
|
useShallow((state) => ({ mapLightCone: state.mapLightCone }))
|
||||||
|
);
|
||||||
|
const { locale } = useLocaleStore(
|
||||||
|
useShallow((state) => ({ locale: state.locale }))
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`bg-base-200/60 rounded-xl p-4 border cursor-pointer transition-all duration-200 ${isSelected
|
className={`group rounded-2xl p-4 border transition-all duration-200 cursor-pointer select-none ${
|
||||||
? 'border-blue-400 ring-2 ring-blue-400/50'
|
isSelected
|
||||||
: 'border-base-300/50 hover:border-base-300 opacity-75'
|
? 'bg-primary/10 border-primary shadow-md ring-2 ring-primary/40'
|
||||||
}`}
|
: 'bg-base-200/80 border-base-300 hover:border-primary/40 hover:bg-base-200'
|
||||||
|
}`}
|
||||||
onClick={() => onProfileToggle(profile)}
|
onClick={() => onProfileToggle(profile)}
|
||||||
>
|
>
|
||||||
{/* Light Cone */}
|
{/* Light Cone */}
|
||||||
{profile.lightcone && (
|
{profile.lightcone && (
|
||||||
<div className="">
|
<div className="p-2 rounded-xl bg-base-100 border border-base-300 text-center mb-3">
|
||||||
<div className="rounded-lg h-42 flex items-center justify-center">
|
<div className="h-32 flex items-center justify-center mb-2">
|
||||||
<Image
|
<Image
|
||||||
unoptimized
|
unoptimized
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
src={`${process.env.CDN_URL}/spriteoutput/lightconemaxfigures/${profile.lightcone.item_id}.png`}
|
src={`${process.env.CDN_URL}/spriteoutput/lightconemaxfigures/${profile.lightcone.item_id}.png`}
|
||||||
alt={getLocaleName(locale, mapLightCone[profile.lightcone.item_id.toString()]?.Name)}
|
alt="Lightcone"
|
||||||
width={348}
|
width={240}
|
||||||
height={408}
|
height={300}
|
||||||
className="w-full h-full object-contain rounded-lg"
|
className="h-full w-auto object-contain rounded-lg transition-transform duration-200 group-hover:scale-105"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-full rounded-lg flex items-center justify-center">
|
<div className="text-sm font-bold truncate">
|
||||||
<div className="text-center">
|
<ParseText
|
||||||
<div className="text-lg font-bold">
|
text={getLocaleName(locale, mapLightCone?.[profile.lightcone.item_id.toString()]?.Name)}
|
||||||
<ParseText
|
locale={locale}
|
||||||
text={getLocaleName(locale, mapLightCone[profile.lightcone.item_id.toString()]?.Name)}
|
/>
|
||||||
locale={locale}
|
</div>
|
||||||
/>
|
<div className="flex items-center justify-center gap-2 mt-1">
|
||||||
</div>
|
<span className="text-xs font-mono font-medium text-base-content/70">Lv.{profile.lightcone.level}</span>
|
||||||
<div className="text-base mb-1">Lv.{profile.lightcone.level} S{profile.lightcone.rank}</div>
|
<span className="text-xs font-mono font-bold text-primary">S{profile.lightcone.rank}</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Relics Mini-grid */}
|
||||||
{Object.keys(profile.relics).length > 0 && (
|
{Object.keys(profile.relics).length > 0 && (
|
||||||
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
|
<div className="flex flex-wrap items-center justify-center gap-1.5 p-1.5 rounded-xl bg-base-100 border border-base-300">
|
||||||
{Object.values(profile.relics).map((relic, index) => (
|
{Object.values(profile.relics).map((relic, index) => (
|
||||||
<div key={index} className="relative">
|
<div key={index} className="relative group/relic">
|
||||||
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50">
|
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-base-200 border border-amber-500/40">
|
||||||
<Image
|
<Image
|
||||||
unoptimized
|
unoptimized
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
|
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.png`}
|
||||||
alt="Relic"
|
alt="Relic"
|
||||||
width={124}
|
width={32}
|
||||||
height={124}
|
height={32}
|
||||||
className="w-14 h-14 object-contain"
|
className="w-6 h-6 object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white text-xs px-1 rounded">
|
<div className="absolute -bottom-1.5 left-1/2 -translate-x-1/2 bg-black/80 text-[9px] font-mono font-bold text-amber-400 px-1 rounded-full border border-white/10">
|
||||||
+{relic.level}
|
+{relic.level}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import useRelicMakerStore from "@/stores/relicMakerStore";
|
import useRelicMakerStore from "@/stores/relicMakerStore";
|
||||||
import useUserDataStore from "@/stores/userDataStore";
|
import useUserDataStore from "@/stores/userDataStore";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
@@ -9,188 +10,126 @@ interface RelicCardProps {
|
|||||||
slot: string
|
slot: string
|
||||||
avatarId: string
|
avatarId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRarityColor = (rarity: string) => {
|
const getRarityColor = (rarity: string) => {
|
||||||
switch (rarity) {
|
switch (rarity) {
|
||||||
case '3': return 'border-green-500 shadow-green-500/50 bg-linear-to-br from-green-700 via-green-400 to-green-500';
|
case '6':
|
||||||
case '4': return 'border-blue-500 shadow-blue-500/50 bg-linear-to-br from-blue-700 via-blue-400 to-blue-500';
|
return 'border-amber-400/90 shadow-[0_0_16px_rgba(251,191,36,0.4)] bg-linear-to-br from-amber-600 via-amber-400 to-amber-500';
|
||||||
case '5': return 'border-purple-500 shadow-purple-500/50 bg-linear-to-br from-purple-700 via-purple-400 to-purple-500';
|
case '5':
|
||||||
case '6': return 'border-yellow-500 shadow-yellow-500/50 bg-linear-to-br from-yellow-700 via-yellow-400 to-yellow-500';
|
return 'border-purple-400/90 shadow-[0_0_16px_rgba(192,132,252,0.4)] bg-linear-to-br from-purple-700 via-purple-400 to-purple-500';
|
||||||
default: return 'border-gray-500 shadow-gray-500/50';
|
case '4':
|
||||||
|
return 'border-sky-400/90 shadow-[0_0_16px_rgba(56,189,248,0.4)] bg-linear-to-br from-blue-700 via-blue-400 to-sky-500';
|
||||||
|
case '3':
|
||||||
|
return 'border-emerald-400/90 shadow-[0_0_16px_rgba(52,211,153,0.4)] bg-linear-to-br from-emerald-700 via-emerald-400 to-emerald-500';
|
||||||
|
default:
|
||||||
|
return 'border-base-content/20 bg-base-300';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRarityName = (slot: string) => {
|
const getSlotIconAndName = (slot: string) => {
|
||||||
switch (slot) {
|
const slotMap: Record<string, { icon: string; name: string }> = {
|
||||||
case '1': return (
|
'1': { icon: '/relics/HEAD.png', name: 'Head' },
|
||||||
<div className="flex items-center gap-1">
|
'2': { icon: '/relics/HAND.png', name: 'Hands' },
|
||||||
<Image
|
'3': { icon: '/relics/BODY.png', name: 'Body' },
|
||||||
unoptimized
|
'4': { icon: '/relics/FOOT.png', name: 'Feet' },
|
||||||
crossOrigin="anonymous"
|
'5': { icon: '/relics/NECK.png', name: 'Planar Sphere' },
|
||||||
src="/relics/HEAD.png"
|
'6': { icon: '/relics/OBJECT.png', name: 'Link Rope' },
|
||||||
alt="Head"
|
};
|
||||||
width={20}
|
|
||||||
height={20}
|
const current = slotMap[slot];
|
||||||
className="bg-black/50 rounded-full"
|
if (!current) return null;
|
||||||
/>
|
|
||||||
<h2>Head</h2>
|
return (
|
||||||
</div>
|
<div className="flex items-center gap-1.5 justify-center mt-2.5">
|
||||||
);
|
<Image
|
||||||
case '2': return (
|
unoptimized
|
||||||
<div className="flex items-center gap-1">
|
crossOrigin="anonymous"
|
||||||
<Image
|
src={current.icon}
|
||||||
unoptimized
|
alt={current.name}
|
||||||
crossOrigin="anonymous"
|
width={18}
|
||||||
src="/relics/HAND.png"
|
height={18}
|
||||||
alt="Hand"
|
className="w-4 h-4 object-contain theme-stat-icon opacity-85"
|
||||||
width={20}
|
/>
|
||||||
height={20}
|
<span className="text-xs font-bold text-base-content">{current.name}</span>
|
||||||
className="bg-black/50 rounded-full"
|
</div>
|
||||||
/>
|
);
|
||||||
<h2>Hands</h2>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case '3': return (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Image
|
|
||||||
unoptimized
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
src="/relics/BODY.png"
|
|
||||||
alt="Body"
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className="bg-black/50 rounded-full"
|
|
||||||
/>
|
|
||||||
<h2>Body</h2>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case '4': return (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Image
|
|
||||||
unoptimized
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
src="/relics/FOOT.png"
|
|
||||||
alt="Foot"
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className="bg-black/50 rounded-full"
|
|
||||||
/>
|
|
||||||
<h2>Feet</h2>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case '5': return (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Image
|
|
||||||
unoptimized
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
src="/relics/NECK.png"
|
|
||||||
alt="Neck"
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className="bg-black/50 rounded-full"
|
|
||||||
/>
|
|
||||||
<h2>Planar sphere</h2>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case '6': return (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Image
|
|
||||||
unoptimized
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
src="/relics/OBJECT.png"
|
|
||||||
alt="Object"
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
className="bg-black/50 rounded-full"
|
|
||||||
/>
|
|
||||||
<h2>Link rope</h2>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
default: return '';
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RelicCard({ slot, avatarId }: RelicCardProps) {
|
export default function RelicCard({ slot, avatarId }: RelicCardProps) {
|
||||||
const { avatars } = useUserDataStore()
|
const { avatars } = useUserDataStore(
|
||||||
const { selectedRelicSlot } = useRelicMakerStore()
|
useShallow((state) => ({ avatars: state.avatars }))
|
||||||
|
)
|
||||||
|
const { selectedRelicSlot } = useRelicMakerStore(
|
||||||
|
useShallow((state) => ({ selectedRelicSlot: state.selectedRelicSlot }))
|
||||||
|
)
|
||||||
|
|
||||||
const relicDetail = useMemo(() => {
|
const relicDetail = useMemo(() => {
|
||||||
const avatar = avatars[avatarId];
|
const avatar = avatars[avatarId];
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
if (avatar.profileList[avatar.profileSelect].relics[slot]) {
|
const profile = avatar.profileList?.[avatar.profileSelect] ?? avatar.profileList?.[0];
|
||||||
return avatar.profileList[avatar.profileSelect].relics[slot];
|
return profile?.relics?.[slot] || null;
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [avatars, avatarId, slot]);
|
}, [avatars, avatarId, slot]);
|
||||||
|
|
||||||
|
const isSelected = selectedRelicSlot === slot;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="flex flex-col items-center select-none group cursor-pointer">
|
||||||
{relicDetail ? (
|
{relicDetail ? (
|
||||||
<div
|
<div className="flex flex-col items-center">
|
||||||
className="flex flex-col items-center cursor-pointer ">
|
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
relative w-24 h-24 rounded-full
|
relative w-22 h-22 sm:w-24 sm:h-24 rounded-full border-3
|
||||||
${getRarityColor(relicDetail.relic_id.toString()[0])}
|
${getRarityColor(relicDetail.relic_id.toString()[0])}
|
||||||
shadow-xl
|
flex items-center justify-center
|
||||||
flex items-center justify-center
|
transition-all duration-200
|
||||||
cursor-pointer transition-transform
|
${isSelected ? 'ring-2 ring-primary/70 shadow-md ring-offset-2 ring-offset-base-100' : 'hover:border-primary/60'}
|
||||||
${selectedRelicSlot === slot ? 'ring-5 ring-success scale-105' : 'ring-3 ring-primary'}
|
`}
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
<span>
|
<Image
|
||||||
<Image
|
unoptimized
|
||||||
unoptimized
|
crossOrigin="anonymous"
|
||||||
crossOrigin="anonymous"
|
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relicDetail.relic_set_id}_${slot}.png`}
|
||||||
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relicDetail.relic_set_id}_${slot}.png`}
|
alt="Relic"
|
||||||
alt="Relic"
|
width={124}
|
||||||
width={124}
|
height={124}
|
||||||
height={124}
|
className="w-14 h-14 sm:w-16 sm:h-16 object-contain drop-shadow-md"
|
||||||
className="w-14 h-14 object-contain"
|
/>
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Level Badge */}
|
{/* Level Badge */}
|
||||||
<div className="absolute -bottom-2 bg-base-100 border-2 border-base-300 rounded-full px-2 py-1">
|
<div className="absolute -bottom-2 bg-base-100 border border-base-content/15 shadow-md rounded-full px-2 py-0.5">
|
||||||
<span className="text-sm font-bold text-primary">+{relicDetail.level}</span>
|
<span className="text-xs font-mono font-bold text-primary">+{relicDetail.level}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 text-center">
|
{getSlotIconAndName(slot)}
|
||||||
<div className="text-sm font-medium text-base-content">{getRarityName(slot)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className="flex flex-col items-center">
|
||||||
className="flex flex-col items-center cursor-pointer">
|
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
relative w-24 h-24 rounded-full border-4
|
relative w-22 h-22 sm:w-24 sm:h-24 rounded-full border-2 border-dashed border-base-content/20
|
||||||
${getRarityColor("None")}
|
bg-base-200/60 flex items-center justify-center
|
||||||
bg-base-300 shadow-xl
|
transition-all duration-200 group-hover:border-primary/60
|
||||||
flex items-center justify-center
|
${isSelected ? 'ring-2 ring-primary/70 border-solid bg-primary/10 ring-offset-2 ring-offset-base-100' : ''}
|
||||||
cursor-pointer hover:scale-105 transition-transform
|
`}
|
||||||
ring-4 ring-primary
|
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
<span className="text-3xl">
|
<div className="flex flex-col items-center justify-center gap-1 text-base-content/40 group-hover:text-primary transition-colors">
|
||||||
<svg className="w-12 h-12 text-base-content/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
{/* Level Badge */}
|
{/* Level Badge */}
|
||||||
<div className="absolute -bottom-2 bg-base-100 border-2 border-base-300 rounded-full px-2 py-1">
|
<div className="absolute -bottom-2 bg-base-100 border border-base-content/15 shadow-md rounded-full px-2 py-0.5">
|
||||||
<span className="text-sm font-bold text-primary">+{0}</span>
|
<span className="text-xs font-mono font-bold text-base-content/40">+0</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 text-center">
|
{getSlotIconAndName(slot)}
|
||||||
<div>{getRarityName(slot)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { AvatarDetail } from '@/types';
|
import { AvatarDetail } from '@/types';
|
||||||
import useDetailDataStore from '@/stores/detailDataStore';
|
import useDetailDataStore from '@/stores/detailDataStore';
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
interface SimpleAvatarCardProps {
|
interface SimpleAvatarCardProps {
|
||||||
data: AvatarDetail;
|
data: AvatarDetail;
|
||||||
@@ -12,58 +13,72 @@ interface SimpleAvatarCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const SimpleAvatarCard = ({ data, isSelected, onClick, showRemoveHover }: SimpleAvatarCardProps) => {
|
export const SimpleAvatarCard = ({ data, isSelected, onClick, showRemoveHover }: SimpleAvatarCardProps) => {
|
||||||
const { baseType, damageType } = useDetailDataStore()
|
const { baseType, damageType } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
baseType: state.baseType,
|
||||||
|
damageType: state.damageType,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const is5Star = data.Rarity === "CombatPowerAvatarRarityType5";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={`relative w-16 h-16 sm:w-20 sm:h-20 rounded-md cursor-pointer transition-transform duration-200 ease-in-out hover:scale-105 shadow-md shrink-0
|
className={`group relative w-16 h-16 sm:w-20 sm:h-20 rounded-xl cursor-pointer shadow-sm hover:shadow-md shrink-0 border border-base-content/10 overflow-hidden select-none ${
|
||||||
${isSelected ? 'ring-2 ring-success opacity-60' : ''}
|
isSelected ? 'ring-2 ring-primary opacity-60 scale-95' : ''
|
||||||
bg-linear-to-br ${data.Rarity === "CombatPowerAvatarRarityType5"
|
}`}
|
||||||
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
|
|
||||||
: "from-purple-400 via-purple-600/70 to-purple-800/50"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<Image
|
<div
|
||||||
width={80}
|
className={`relative w-full h-full rounded-xl overflow-hidden flex items-center justify-center bg-linear-to-b ${
|
||||||
height={80}
|
is5Star
|
||||||
unoptimized
|
? "from-yellow-400 via-amber-500/85 to-amber-700/70"
|
||||||
crossOrigin="anonymous"
|
: "from-purple-400 via-purple-600/80 to-purple-800/60"
|
||||||
src={`${process.env.CDN_URL}/${data.Image.ActionAvatarHeadIconPath}`}
|
}`}
|
||||||
priority={true}
|
>
|
||||||
className="w-full h-full object-contain"
|
|
||||||
alt="Avatar"
|
|
||||||
/>
|
|
||||||
<div className="absolute top-0 left-0 w-5 h-5 bg-black/40 rounded-full flex items-center justify-center p-0.5">
|
|
||||||
<Image
|
<Image
|
||||||
width={20}
|
width={80}
|
||||||
height={20}
|
height={80}
|
||||||
unoptimized
|
unoptimized
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
src={`${process.env.CDN_URL}/${damageType?.[data.DamageType]?.Icon}`}
|
src={`${process.env.CDN_URL}/${data.Image.ActionAvatarHeadIconPath}`}
|
||||||
className="w-full h-full object-contain"
|
priority={true}
|
||||||
alt="Element"
|
className="w-full h-full object-contain transition-transform duration-200 group-hover:scale-105"
|
||||||
|
alt="Avatar"
|
||||||
/>
|
/>
|
||||||
</div>
|
{damageType?.[data.DamageType]?.Icon && (
|
||||||
<div className="absolute top-0 right-0 w-5 h-5 bg-black/40 rounded-full flex items-center justify-center p-0.5">
|
<div className="absolute top-0.5 left-0.5 w-4 h-4 sm:w-5 sm:h-5 bg-black/60 backdrop-blur-xs rounded-full flex items-center justify-center p-0.5 border border-white/10">
|
||||||
<Image
|
<Image
|
||||||
width={20}
|
width={20}
|
||||||
height={20}
|
height={20}
|
||||||
unoptimized
|
unoptimized
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
src={`${process.env.CDN_URL}/${baseType?.[data.BaseType]?.Icon}`}
|
src={`${process.env.CDN_URL}/${damageType[data.DamageType].Icon}`}
|
||||||
className="w-full h-full object-contain"
|
className="w-full h-full object-contain"
|
||||||
alt="Path"
|
alt="Element"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{baseType?.[data.BaseType]?.Icon && (
|
||||||
|
<div className="absolute top-0.5 right-0.5 w-4 h-4 sm:w-5 sm:h-5 bg-black/60 backdrop-blur-xs rounded-full flex items-center justify-center p-0.5 border border-white/10">
|
||||||
|
<Image
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src={`${process.env.CDN_URL}/${baseType[data.BaseType].Icon}`}
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
alt="Path"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{showRemoveHover && (
|
{showRemoveHover && (
|
||||||
<div className="absolute inset-0 bg-error/80 rounded-md flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity">
|
<div className="absolute inset-0 bg-error/90 backdrop-blur-xs rounded-xl flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<X className="h-6 w-6 text-white" />
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
</div>
|
||||||
</svg>
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,33 +1,35 @@
|
|||||||
|
|
||||||
import useLocaleStore from '@/stores/localeStore';
|
import useLocaleStore from '@/stores/localeStore';
|
||||||
import { Check, Sparkles, Bug, Zap, Package, Calendar } from 'lucide-react';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
import { Sparkles, Bug, Zap, Package, Calendar } from 'lucide-react';
|
||||||
|
|
||||||
export default function ChangelogBar() {
|
export default function ChangelogBar() {
|
||||||
const { changelog } = useLocaleStore()
|
const { changelog } = useLocaleStore(
|
||||||
|
useShallow((state) => ({ changelog: state.changelog }))
|
||||||
|
)
|
||||||
|
|
||||||
const getIcon = (type: string) => {
|
const getIcon = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'feature':
|
case 'feature':
|
||||||
return <Sparkles className="w-3 h-3 md:w-4 md:h-4" />;
|
return <Sparkles className="w-3.5 h-3.5" />;
|
||||||
case 'fix':
|
case 'fix':
|
||||||
return <Bug className="w-3 h-3 md:w-4 md:h-4" />;
|
return <Bug className="w-3.5 h-3.5" />;
|
||||||
case 'improvement':
|
case 'improvement':
|
||||||
return <Zap className="w-3 h-3 md:w-4 md:h-4" />;
|
return <Zap className="w-3.5 h-3.5" />;
|
||||||
default:
|
default:
|
||||||
return <Package className="w-3 h-3 md:w-4 md:h-4" />;
|
return <Package className="w-3.5 h-3.5" />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBadgeClass = (type: string) => {
|
const getBadgeClass = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'feature':
|
case 'feature':
|
||||||
return 'badge-success';
|
return 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30';
|
||||||
case 'fix':
|
case 'fix':
|
||||||
return 'badge-error';
|
return 'bg-rose-500/15 text-rose-400 border-rose-500/30';
|
||||||
case 'improvement':
|
case 'improvement':
|
||||||
return 'badge-warning';
|
return 'bg-amber-500/15 text-amber-400 border-amber-500/30';
|
||||||
default:
|
default:
|
||||||
return 'badge-info';
|
return 'bg-primary/15 text-primary border-primary/30';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,44 +47,37 @@ export default function ChangelogBar() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="md:p-4">
|
<div className="space-y-4">
|
||||||
{/* Alert */}
|
<div className="space-y-3">
|
||||||
{/* <div className="alert alert-info mb-4 md:p-2 p-1">
|
|
||||||
<AlertCircle className="w-6 h-6" />
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
{/* Timeline */}
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
{changelog.map((change, index) => (
|
{changelog.map((change, index) => (
|
||||||
<div key={index} className="bg-base-100 shadow-sm">
|
<div key={index} className="p-4 sm:p-5 rounded-2xl bg-base-200/60 border border-white/10 shadow-lg space-y-3">
|
||||||
<div className="flex flex-col gap-2">
|
{/* Version Header */}
|
||||||
{/* Version Header */}
|
<div className="flex flex-wrap items-center justify-between gap-2 pb-2.5 border-b border-base-content/10">
|
||||||
<div className="flex flex-row items-center gap-2 md:gap-3">
|
<div className="flex items-center gap-2">
|
||||||
<div className={`badge ${getBadgeClass(change.type)} gap-1 md:gap-2 p-1`}>
|
<span className={`px-2.5 py-1 rounded-full text-xs font-mono font-bold flex items-center gap-1.5 border ${getBadgeClass(change.type)}`}>
|
||||||
{getIcon(change.type)}
|
{getIcon(change.type)}
|
||||||
{getTypeLabel(change.type)}
|
{getTypeLabel(change.type)}
|
||||||
</div>
|
</span>
|
||||||
<h2 className="card-title text-sm md:text-lg">
|
<h3 className="font-extrabold text-sm sm:text-base tracking-tight">
|
||||||
Version {change.version}
|
Version {change.version}
|
||||||
</h2>
|
</h3>
|
||||||
<div className="flex items-center gap-1 md:gap-2 text-base-content/60 ml-auto">
|
|
||||||
<Calendar className="w-3 h-3 md:w-4 md:h-4" />
|
|
||||||
<span className="text-sm">{change.date}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Changes List */}
|
<div className="flex items-center gap-1.5 text-xs text-base-content/50 font-mono">
|
||||||
<ul className="space-y-3">
|
<Calendar className="w-3.5 h-3.5" />
|
||||||
{change.items.map((item, itemIndex) => (
|
<span>{change.date}</span>
|
||||||
<li key={itemIndex} className="flex items-start gap-3">
|
</div>
|
||||||
<div className="mt-1">
|
|
||||||
<Check className="w-5 h-5 text-success" />
|
|
||||||
</div>
|
|
||||||
<span className="text-base-content">{item}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Changes List */}
|
||||||
|
<ul className="space-y-2 pt-1 text-xs sm:text-sm text-base-content/85">
|
||||||
|
{change.items.map((item, itemIndex) => (
|
||||||
|
<li key={itemIndex} className="flex items-start gap-2.5">
|
||||||
|
<div className="mt-1 w-1.5 h-1.5 rounded-full bg-primary shrink-0" />
|
||||||
|
<span className="leading-relaxed">{item}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+156
-122
@@ -6,6 +6,8 @@ import useGlobalStore from "@/stores/globalStore"
|
|||||||
import { PSConnectType } from "@/types"
|
import { PSConnectType } from "@/types"
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
import { useShallow } from "zustand/react/shallow"
|
||||||
|
import { Server, RefreshCw, AlertCircle } from "lucide-react"
|
||||||
|
|
||||||
export default function ConnectBar() {
|
export default function ConnectBar() {
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
@@ -21,149 +23,181 @@ export default function ConnectBar() {
|
|||||||
setServerUrl,
|
setServerUrl,
|
||||||
setUsername,
|
setUsername,
|
||||||
setPassword
|
setPassword
|
||||||
} = useConnectStore()
|
} = useConnectStore(
|
||||||
const { isConnectPS, setIsConnectPS } = useGlobalStore()
|
useShallow((state) => ({
|
||||||
|
connectionType: state.connectionType,
|
||||||
|
privateType: state.privateType,
|
||||||
|
serverUrl: state.serverUrl,
|
||||||
|
username: state.username,
|
||||||
|
password: state.password,
|
||||||
|
setConnectionType: state.setConnectionType,
|
||||||
|
setPrivateType: state.setPrivateType,
|
||||||
|
setServerUrl: state.setServerUrl,
|
||||||
|
setUsername: state.setUsername,
|
||||||
|
setPassword: state.setPassword,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { isConnectPS, setIsConnectPS } = useGlobalStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
isConnectPS: state.isConnectPS,
|
||||||
|
setIsConnectPS: state.setIsConnectPS,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-6 py-4">
|
<div className="space-y-4">
|
||||||
<div className="form-control grid grid-cols-1 w-full mb-6">
|
<div className="p-4 sm:p-6 rounded-2xl bg-base-200/60 border border-white/10 space-y-4">
|
||||||
<label className="label">
|
{/* Connection Type Selection */}
|
||||||
<span className="label-text font-semibold text-purple-300">{transI18n("connectionType")}</span>
|
<div className="space-y-1.5">
|
||||||
</label>
|
<label className="block text-xs font-bold uppercase tracking-wider text-base-content/70">
|
||||||
<select
|
{transI18n("connectionType")}
|
||||||
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
|
</label>
|
||||||
value={connectionType}
|
<select
|
||||||
onChange={(e) => {
|
className="select select-bordered w-full rounded-xl bg-base-100 border-base-content/10 focus:border-primary text-sm font-medium"
|
||||||
setIsConnectPS(false)
|
value={connectionType}
|
||||||
setConnectionType(e.target.value)
|
onChange={(e) => {
|
||||||
}}
|
setIsConnectPS(false)
|
||||||
>
|
setConnectionType(e.target.value)
|
||||||
<option value={PSConnectType.FireflyGo}>FireflyGo</option>
|
|
||||||
<option value={PSConnectType.RobinSR}>RobinSR</option>
|
|
||||||
<option value={PSConnectType.Other}>{transI18n("other")}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{connectionType === PSConnectType.Other && (
|
|
||||||
<div className="flex flex-col md:space-x-4 mb-6 gap-2">
|
|
||||||
<div className="form-control w-full mb-4 md:mb-0">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-semibold text-purple-300">{transI18n("serverUrl")}</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={transI18n("placeholderServerUrl")}
|
|
||||||
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
|
|
||||||
value={serverUrl}
|
|
||||||
onChange={(e) => setServerUrl(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-control w-full mb-4 md:mb-0">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-semibold text-purple-300">{transI18n("privateType")}</span>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
|
|
||||||
value={privateType}
|
|
||||||
onChange={(e) => setPrivateType(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="Local">{transI18n("local")}</option>
|
|
||||||
<option value="Server">{transI18n("server")}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="form-control w-full mb-4 md:mb-0">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-semibold text-purple-300">{transI18n("username")}</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={transI18n("placeholderUsername")}
|
|
||||||
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-control w-full mb-4 md:mb-0">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-semibold text-purple-300">{transI18n("password")}</span>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
placeholder={transI18n("placeholderPassword")}
|
|
||||||
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{message.text && (
|
|
||||||
<div className={`alert ${message.type === 'success' ? 'alert-success' :
|
|
||||||
message.type === 'error' ? 'alert-error' : 'alert-info'
|
|
||||||
} mb-6`}>
|
|
||||||
<span>{message.text}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 mb-2">
|
|
||||||
<div className="flex items-center justify-center md:justify-start">
|
|
||||||
<span className="text-md mr-2">{transI18n("status")}:</span>
|
|
||||||
<span
|
|
||||||
className={`badge ${isConnectPS ? "badge-success" : "badge-error"
|
|
||||||
} badge-lg`}
|
|
||||||
>
|
|
||||||
{isConnectPS ? transI18n("connected") : transI18n("unconnected")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Buttons */}
|
|
||||||
<div className="flex flex-col sm:flex-row gap-2 w-full justify-center md:justify-end">
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
const response = await connectToPS();
|
|
||||||
if (response.success) {
|
|
||||||
setMessage({
|
|
||||||
type: "success",
|
|
||||||
text: transI18n("connectedSuccess"),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setMessage({
|
|
||||||
type: "error",
|
|
||||||
text: response.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
className="btn btn-primary w-full sm:w-auto"
|
|
||||||
>
|
>
|
||||||
{transI18n("connectPs")}
|
<option value={PSConnectType.FireflyGo}>FireflyGo</option>
|
||||||
</button>
|
<option value={PSConnectType.RobinSR}>RobinSR</option>
|
||||||
|
<option value={PSConnectType.Other}>{transI18n("other")}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
{isConnectPS && (
|
{connectionType === PSConnectType.Other && (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="block text-xs font-semibold text-base-content/70">
|
||||||
|
{transI18n("serverUrl")}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={transI18n("placeholderServerUrl")}
|
||||||
|
className="input input-sm sm:input-md input-bordered w-full rounded-xl bg-base-100 border-base-content/10 focus:border-primary text-xs sm:text-sm"
|
||||||
|
value={serverUrl}
|
||||||
|
onChange={(e) => setServerUrl(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="block text-xs font-semibold text-base-content/70">
|
||||||
|
{transI18n("privateType")}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
className="select select-sm sm:select-md select-bordered w-full rounded-xl bg-base-100 border-base-content/10 focus:border-primary text-xs sm:text-sm"
|
||||||
|
value={privateType}
|
||||||
|
onChange={(e) => setPrivateType(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="Local">{transI18n("local")}</option>
|
||||||
|
<option value="Server">{transI18n("server")}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="block text-xs font-semibold text-base-content/70">
|
||||||
|
{transI18n("username")}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={transI18n("placeholderUsername")}
|
||||||
|
className="input input-sm sm:input-md input-bordered w-full rounded-xl bg-base-100 border-base-content/10 focus:border-primary text-xs sm:text-sm"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="block text-xs font-semibold text-base-content/70">
|
||||||
|
{transI18n("password")}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
placeholder={transI18n("placeholderPassword")}
|
||||||
|
className="input input-sm sm:input-md input-bordered w-full rounded-xl bg-base-100 border-base-content/10 focus:border-primary text-xs sm:text-sm"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Status Message Notification */}
|
||||||
|
{message.text && (
|
||||||
|
<div className={`p-3 rounded-xl border text-xs flex items-center gap-2 ${
|
||||||
|
message.type === 'success'
|
||||||
|
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400'
|
||||||
|
: message.type === 'error'
|
||||||
|
? 'bg-rose-500/10 border-rose-500/30 text-rose-400'
|
||||||
|
: 'bg-info/10 border-info/30 text-info'
|
||||||
|
}`}>
|
||||||
|
<AlertCircle className="w-4 h-4 shrink-0" />
|
||||||
|
<span>{message.text}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Footer Controls */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 pt-3 border-t border-base-content/10">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-base-content/60">{transI18n("status")}:</span>
|
||||||
|
<div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-mono font-bold border ${
|
||||||
|
isConnectPS
|
||||||
|
? "bg-emerald-500/15 border-emerald-500/30 text-emerald-400"
|
||||||
|
: "bg-rose-500/15 border-rose-500/30 text-rose-400"
|
||||||
|
}`}>
|
||||||
|
<span className={`w-2 h-2 rounded-full ${isConnectPS ? 'bg-emerald-400 animate-pulse' : 'bg-rose-400'}`}></span>
|
||||||
|
<span>{isConnectPS ? transI18n("connected") : transI18n("unconnected")}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 w-full sm:w-auto">
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const response = await syncDataToPS();
|
const response = await connectToPS();
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
setMessage({
|
setMessage({
|
||||||
type: "success",
|
type: "success",
|
||||||
text: transI18n("syncSuccess"),
|
text: transI18n("connectedSuccess"),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setMessage({
|
setMessage({
|
||||||
type: "error",
|
type: "error",
|
||||||
text: `${transI18n("syncFailed")}: ${response.message}`,
|
text: response.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="btn btn-success w-full sm:w-auto"
|
className="btn btn-sm btn-primary rounded-xl font-bold flex-1 sm:flex-none shadow-xs"
|
||||||
>
|
>
|
||||||
{transI18n("sync")}
|
<Server className="w-3.5 h-3.5" />
|
||||||
|
{transI18n("connectPs")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
|
{isConnectPS && (
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
const response = await syncDataToPS();
|
||||||
|
if (response.success) {
|
||||||
|
setMessage({
|
||||||
|
type: "success",
|
||||||
|
text: transI18n("syncSuccess"),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setMessage({
|
||||||
|
type: "error",
|
||||||
|
text: `${transI18n("syncFailed")}: ${response.message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="btn btn-sm btn-accent rounded-xl font-bold flex-1 sm:flex-none shadow-xs"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-3.5 h-3.5" />
|
||||||
|
{transI18n("sync")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -8,13 +8,22 @@ import { useMemo } from "react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||||
import ExtraEffectList from '../extraInfo';
|
import ExtraEffectList from '../extraInfo';
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
|
||||||
export default function EidolonsInfo() {
|
export default function EidolonsInfo() {
|
||||||
const { avatarSelected } = useCurrentDataStore()
|
const { avatarSelected } = useCurrentDataStore(
|
||||||
const { locale } = useLocaleStore()
|
useShallow((state) => ({ avatarSelected: state.avatarSelected }))
|
||||||
|
)
|
||||||
|
const { locale } = useLocaleStore(
|
||||||
|
useShallow((state) => ({ locale: state.locale }))
|
||||||
|
)
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
const { setAvatars, avatars } = useUserDataStore()
|
const { setAvatars, avatars } = useUserDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
setAvatars: state.setAvatars,
|
||||||
|
avatars: state.avatars,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
const charRank = useMemo(() => {
|
const charRank = useMemo(() => {
|
||||||
if (!avatarSelected) return null;
|
if (!avatarSelected) return null;
|
||||||
@@ -25,58 +34,106 @@ export default function EidolonsInfo() {
|
|||||||
return avatarSelected?.Ranks
|
return avatarSelected?.Ranks
|
||||||
}, [avatarSelected, avatars]);
|
}, [avatarSelected, avatars]);
|
||||||
|
|
||||||
|
const currentRank = avatars[avatarSelected?.ID.toString() || ""]?.data?.rank || 0;
|
||||||
|
|
||||||
|
const handleToggleRank = (rankNum: number) => {
|
||||||
|
if (!avatarSelected) return;
|
||||||
|
let newRank = rankNum;
|
||||||
|
if (currentRank === rankNum) {
|
||||||
|
newRank = rankNum - 1;
|
||||||
|
}
|
||||||
|
setAvatars({
|
||||||
|
...avatars,
|
||||||
|
[avatarSelected.ID]: {
|
||||||
|
...avatars[avatarSelected.ID],
|
||||||
|
data: {
|
||||||
|
...avatars[avatarSelected.ID]?.data,
|
||||||
|
rank: newRank,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
|
<div className="w-full">
|
||||||
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
|
<div className="bg-base-100 rounded-2xl p-4 sm:p-6 border border-base-300 shadow-sm space-y-6">
|
||||||
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
|
{/* Header */}
|
||||||
{transI18n("eidolons")}
|
<div className="flex items-center justify-between pb-3 border-b border-base-300">
|
||||||
</h2>
|
<h2 className="text-lg sm:text-xl font-bold flex items-center gap-2.5">
|
||||||
<div className="grid grid-cols-1 m-4 p-4 font-bold gap-4 w-fit max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
|
<div className="w-1.5 h-5 bg-linear-to-b from-primary to-accent rounded-full"></div>
|
||||||
|
<span className="bg-clip-text text-transparent bg-linear-to-r from-primary to-accent font-bold tracking-tight">
|
||||||
|
{transI18n("eidolons")}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-base-200 border border-base-300 text-xs font-mono font-bold">
|
||||||
|
<span className="text-base-content/60">Active:</span>
|
||||||
|
<span className="text-primary text-sm font-extrabold">E{currentRank}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 6 Eidolons Organic Grid */}
|
||||||
{charRank && avatars[avatarSelected?.ID || ""] && (
|
{charRank && avatars[avatarSelected?.ID || ""] && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 lg:gap-8">
|
||||||
{Object.entries(charRank || {}).map(([key, rank]) => (
|
{Object.entries(charRank || {}).map(([key, rank]) => {
|
||||||
<div key={key}
|
const rankNum = Number(rank.Rank);
|
||||||
className="flex flex-col items-center"
|
const isUnlocked = rankNum <= currentRank;
|
||||||
|
return (
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className="cursor-pointer"
|
key={key}
|
||||||
onClick={() => {
|
className="flex flex-col items-center text-center space-y-3 p-2"
|
||||||
let newRank = Number(rank.Rank)
|
|
||||||
if (avatars[avatarSelected?.ID || ""]?.data?.rank == Number(rank.Rank)) {
|
|
||||||
newRank = Number(rank.Rank) - 1
|
|
||||||
}
|
|
||||||
setAvatars({ ...avatars, [avatarSelected?.ID || ""]: { ...avatars[avatarSelected?.ID || ""], data: { ...avatars[avatarSelected?.ID || ""].data, rank: newRank } } })
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Image
|
{/* 1. Organic Floating Eidolon Shard (No artificial rectangle wrapper) */}
|
||||||
className={`w-60 object-contain mb-2 ${Number(rank.Rank) <= avatars[avatarSelected?.ID.toString() || ""]?.data?.rank ? "" : "grayscale"}`}
|
<div
|
||||||
src={`${process.env.CDN_URL}/ui/ui3d/rank/_dependencies/textures/${avatarSelected?.ID}/${avatarSelected?.ID}_Rank_${rank.Rank}.png`}
|
onClick={() => handleToggleRank(rankNum)}
|
||||||
alt={`Rank ${rank.Rank} Image`}
|
className="cursor-pointer select-none transition-transform duration-200 hover:scale-105 active:scale-95"
|
||||||
priority
|
title={`Click to ${isUnlocked ? 'deactivate' : 'activate'} E${rank.Rank}`}
|
||||||
unoptimized
|
>
|
||||||
crossOrigin="anonymous"
|
<Image
|
||||||
width={240}
|
className={`w-44 sm:w-52 h-auto object-contain drop-shadow-md transition-all duration-300 ${
|
||||||
height={240}
|
isUnlocked
|
||||||
/>
|
? "drop-shadow-[0_0_12px_rgba(var(--color-primary),0.35)]"
|
||||||
|
: "grayscale opacity-40 hover:opacity-70"
|
||||||
|
}`}
|
||||||
|
src={`${process.env.CDN_URL}/ui/ui3d/rank/_dependencies/textures/${avatarSelected?.ID}/${avatarSelected?.ID}_rank_${rank.Rank}.png`}
|
||||||
|
alt={`Rank ${rank.Rank} Image`}
|
||||||
|
priority
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
width={240}
|
||||||
|
height={240}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="text-lg pb-1 flex items-center justify-items-center gap-2">
|
{/* 2. Eidolon Name & Rank */}
|
||||||
<span className="inline-block text-indigo-500">{rank.Rank}.</span>
|
<div
|
||||||
|
onClick={() => handleToggleRank(rankNum)}
|
||||||
|
className="flex items-center justify-center gap-1.5 cursor-pointer group select-none"
|
||||||
|
>
|
||||||
|
<span className={`text-base font-extrabold font-mono ${
|
||||||
|
isUnlocked ? "text-primary" : "text-base-content/50"
|
||||||
|
}`}>
|
||||||
|
E{rank.Rank}.
|
||||||
|
</span>
|
||||||
<ParseText
|
<ParseText
|
||||||
locale={locale}
|
locale={locale}
|
||||||
text={getLocaleName(locale, rank.Name)}
|
text={getLocaleName(locale, rank.Name)}
|
||||||
className="text-center text-base font-normal leading-tight"
|
className="font-bold text-base text-base-content group-hover:text-primary transition-colors"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-sm font-normal">
|
{/* 3. Description & Extra Effects (Direct readable text, fully selectable & copyable) */}
|
||||||
<div dangerouslySetInnerHTML={{ __html: replaceByParam(getLocaleName(locale, rank.Desc), rank.Param) }} />
|
<div className="w-full text-left text-xs sm:text-sm text-base-content/85 leading-relaxed space-y-2 select-text pt-1 border-t border-base-300/60">
|
||||||
<ExtraEffectList extras={rank?.Extra} locale={locale} />
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: replaceByParam(getLocaleName(locale, rank.Desc), rank.Param),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ExtraEffectList extras={rank?.Extra} locale={locale} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { motion } from "framer-motion"
|
|
||||||
import { EyeOff, Eye, Hammer, RefreshCw, ShieldBan, User, Swords, SkipForward, BowArrow, Info, RouteIcon, Search, CupSoda, UsersIcon } from "lucide-react"
|
import { EyeOff, Eye, Hammer, RefreshCw, ShieldBan, User, Swords, SkipForward, BowArrow, Info, RouteIcon, Search, CupSoda, UsersIcon } from "lucide-react"
|
||||||
import useGlobalStore from '@/stores/globalStore'
|
import useGlobalStore from '@/stores/globalStore'
|
||||||
import { useTranslations } from "next-intl"
|
import { useTranslations } from "next-intl"
|
||||||
@@ -13,12 +12,31 @@ import Prism from "prismjs"
|
|||||||
import "prismjs/components/prism-lua"
|
import "prismjs/components/prism-lua"
|
||||||
import "prismjs/themes/prism-tomorrow.css"
|
import "prismjs/themes/prism-tomorrow.css"
|
||||||
import { SimpleAvatarCard } from "../card/simpleCharacterCard"
|
import { SimpleAvatarCard } from "../card/simpleCharacterCard"
|
||||||
|
import { useShallow } from "zustand/react/shallow"
|
||||||
|
|
||||||
export default function ExtraSettingBar() {
|
export default function ExtraSettingBar() {
|
||||||
const { extraData, setExtraData, isEnableChangePath, setIsEnableChangePath, isEnableLua, setIsEnableLua } = useGlobalStore()
|
const { extraData, setExtraData, isEnableChangePath, setIsEnableChangePath, isEnableLua, setIsEnableLua } = useGlobalStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
extraData: state.extraData,
|
||||||
|
setExtraData: state.setExtraData,
|
||||||
|
isEnableChangePath: state.isEnableChangePath,
|
||||||
|
setIsEnableChangePath: state.setIsEnableChangePath,
|
||||||
|
isEnableLua: state.isEnableLua,
|
||||||
|
setIsEnableLua: state.setIsEnableLua,
|
||||||
|
}))
|
||||||
|
)
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
const { mapAvatar, mapPeak, stage, baseType } = useDetailDataStore()
|
const { mapAvatar, mapPeak, stage, baseType } = useDetailDataStore(
|
||||||
const { locale } = useLocaleStore()
|
useShallow((state) => ({
|
||||||
|
mapAvatar: state.mapAvatar,
|
||||||
|
mapPeak: state.mapPeak,
|
||||||
|
stage: state.stage,
|
||||||
|
baseType: state.baseType,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { locale } = useLocaleStore(
|
||||||
|
useShallow((state) => ({ locale: state.locale }))
|
||||||
|
)
|
||||||
const [showSearchStage, setShowSearchStage] = useState(false)
|
const [showSearchStage, setShowSearchStage] = useState(false)
|
||||||
const [showSearchLineup, setShowSearchLineup] = useState(false)
|
const [showSearchLineup, setShowSearchLineup] = useState(false)
|
||||||
const [isChildClick, setIsChildClick] = useState(false)
|
const [isChildClick, setIsChildClick] = useState(false)
|
||||||
@@ -48,200 +66,242 @@ export default function ExtraSettingBar() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-4 sm:px-6 py-4 space-y-8">
|
<div className="space-y-6">
|
||||||
|
{/* THEORY CRAFT */}
|
||||||
{extraData?.theory_craft && (
|
{extraData?.theory_craft && (
|
||||||
<div className="space-y-4">
|
<div className="p-4 sm:p-5 rounded-2xl bg-base-200/60 border border-white/10 shadow-lg space-y-4">
|
||||||
<h3 className="text-lg font-bold flex items-center gap-2">
|
<div className="flex items-center justify-between pb-2 border-b border-base-content/10">
|
||||||
{transI18n("theoryCraft")}
|
<h3 className="text-sm font-bold uppercase tracking-wider text-primary flex items-center gap-2">
|
||||||
<div className="tooltip tooltip-info tooltip-bottom" data-tip={transI18n("detailTheoryCraft")}>
|
<span className="w-1.5 h-3.5 bg-primary rounded-full"></span>
|
||||||
<Info className="text-primary" size={20} />
|
{transI18n("theoryCraft")}
|
||||||
|
</h3>
|
||||||
|
<div className="tooltip tooltip-left tooltip-info" data-tip={transI18n("detailTheoryCraft")}>
|
||||||
|
<Info className="text-primary w-4 h-4 cursor-pointer" />
|
||||||
</div>
|
</div>
|
||||||
</h3>
|
</div>
|
||||||
|
|
||||||
<motion.div className="form-control bg-base-200 p-4 rounded-xl shadow">
|
<div className="p-3.5 rounded-xl bg-base-100/60 border border-white/5 flex items-center justify-between">
|
||||||
<label className="flex flex-wrap items-center cursor-pointer justify-start gap-3">
|
<label className="flex items-center gap-3 cursor-pointer">
|
||||||
<Hammer className="text-primary" size={20} />
|
<Hammer className="text-primary w-4 h-4" />
|
||||||
<input
|
<span className="text-xs sm:text-sm font-semibold">{transI18n("theoryCraftMode")}</span>
|
||||||
type="checkbox"
|
|
||||||
className="toggle toggle-primary"
|
|
||||||
checked={extraData?.theory_craft?.mode}
|
|
||||||
onChange={(e) => {
|
|
||||||
setExtraData({
|
|
||||||
...extraData,
|
|
||||||
theory_craft: {
|
|
||||||
hp: extraData?.theory_craft?.hp || {},
|
|
||||||
cycle_count: extraData?.theory_craft?.cycle_count || 1,
|
|
||||||
mode: e.target.checked,
|
|
||||||
stage_id: extraData?.theory_craft?.stage_id || 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span className="label-text font-semibold">{transI18n("theoryCraftMode")}</span>
|
|
||||||
</label>
|
</label>
|
||||||
</motion.div>
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="toggle toggle-primary"
|
||||||
|
checked={extraData?.theory_craft?.mode}
|
||||||
|
onChange={(e) => {
|
||||||
|
setExtraData({
|
||||||
|
...extraData,
|
||||||
|
theory_craft: {
|
||||||
|
hp: extraData?.theory_craft?.hp || {},
|
||||||
|
cycle_count: extraData?.theory_craft?.cycle_count || 1,
|
||||||
|
mode: e.target.checked,
|
||||||
|
stage_id: extraData?.theory_craft?.stage_id || 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{extraData?.theory_craft?.mode && (
|
{extraData?.theory_craft?.mode && (
|
||||||
<>
|
<div className="space-y-3 pt-1">
|
||||||
<motion.div className="form-control bg-base-200 p-4 rounded-xl shadow">
|
{/* Cycle Count */}
|
||||||
<label className="flex flex-wrap items-center justify-start gap-3">
|
<div className="p-3.5 rounded-xl bg-base-100/60 border border-white/5 flex items-center justify-between gap-3">
|
||||||
<RefreshCw className="text-info" size={20} />
|
<div className="flex items-center gap-2">
|
||||||
<span className="label-text font-semibold">{transI18n("cycleCount")}</span>
|
<RefreshCw className="text-info w-4 h-4" />
|
||||||
<input
|
<span className="text-xs sm:text-sm font-semibold">{transI18n("cycleCount")}</span>
|
||||||
type="number"
|
</div>
|
||||||
className="input input-primary"
|
<input
|
||||||
value={extraData?.theory_craft?.cycle_count}
|
type="number"
|
||||||
onChange={(e) =>
|
className="input input-sm input-bordered w-24 rounded-xl text-center font-mono font-bold"
|
||||||
setExtraData({
|
value={extraData?.theory_craft?.cycle_count}
|
||||||
...extraData,
|
onChange={(e) =>
|
||||||
theory_craft: {
|
setExtraData({
|
||||||
hp: extraData?.theory_craft?.hp || {},
|
...extraData,
|
||||||
cycle_count: Number(e.target.value) || 1,
|
theory_craft: {
|
||||||
mode: extraData?.theory_craft?.mode || false,
|
hp: extraData?.theory_craft?.hp || {},
|
||||||
stage_id: extraData?.theory_craft?.stage_id || 0
|
cycle_count: Number(e.target.value) || 1,
|
||||||
}
|
mode: extraData?.theory_craft?.mode || false,
|
||||||
})
|
stage_id: extraData?.theory_craft?.stage_id || 0
|
||||||
}
|
}
|
||||||
min="1"
|
})
|
||||||
/>
|
}
|
||||||
</label>
|
min="1"
|
||||||
</motion.div>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<motion.div className="form-control bg-base-200 p-4 rounded-xl shadow">
|
{/* Stage Selection */}
|
||||||
<label className="flex flex-wrap items-center justify-start gap-3">
|
<div className="p-3.5 rounded-xl bg-base-100/60 border border-white/5 space-y-2">
|
||||||
<RouteIcon className="text-info" size={20} />
|
<div className="flex items-center gap-2">
|
||||||
<span className="label-text font-semibold">{transI18n("stage")}</span>
|
<RouteIcon className="text-info w-4 h-4" />
|
||||||
<div className="relative">
|
<span className="text-xs sm:text-sm font-semibold">{transI18n("stage")}</span>
|
||||||
<div className="flex items-center justify-center gap-2">
|
</div>
|
||||||
<button
|
<div className="relative">
|
||||||
className="btn btn-outline w-full text-left flex items-center gap-2"
|
<button
|
||||||
onClick={(e) => {
|
className="btn btn-sm btn-outline w-full rounded-xl flex items-center justify-between text-xs sm:text-sm"
|
||||||
e.stopPropagation()
|
onClick={(e) => {
|
||||||
if (!isChildClick) {
|
e.stopPropagation()
|
||||||
setShowSearchStage(pre => !pre)
|
if (!isChildClick) {
|
||||||
}
|
setShowSearchStage(pre => !pre)
|
||||||
setIsChildClick(false)
|
}
|
||||||
}}
|
setIsChildClick(false)
|
||||||
>
|
}}
|
||||||
<Search className="w-6 h-6" />
|
>
|
||||||
<span className="text-left"> {transI18n("stage")}: {getLocaleName(locale, stageList.find((s) => s.ID.toString() === extraData?.theory_craft?.stage_id?.toString())?.Name) || transI18n("selectStage")}</span>
|
<span className="truncate">
|
||||||
</button>
|
{getLocaleName(locale, stageList.find((s) => s.ID.toString() === extraData?.theory_craft?.stage_id?.toString())?.Name) || transI18n("selectStage")}
|
||||||
</div>
|
</span>
|
||||||
{showSearchStage && (
|
<Search className="w-3.5 h-3.5 shrink-0 ml-2" />
|
||||||
<div onClick={(e) => e.stopPropagation()} className="absolute top-full mt-2 w-full z-50 border bg-base-200 border-slate-600 rounded-lg p-4 shadow-lg">
|
</button>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
|
|
||||||
<label className="input w-full">
|
{showSearchStage && (
|
||||||
<svg className="h-[1em] opacity-50" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
<div onClick={(e) => e.stopPropagation()} className="absolute top-full mt-2 w-full z-50 bg-base-100 border border-base-content/15 rounded-2xl p-3.5 shadow-2xl space-y-3">
|
||||||
<g
|
<input
|
||||||
strokeLinejoin="round"
|
type="search"
|
||||||
strokeLinecap="round"
|
className="input input-sm input-bordered w-full rounded-xl text-xs"
|
||||||
strokeWidth="2.5"
|
placeholder={transI18n("searchStage")}
|
||||||
fill="none"
|
value={stageSearchTerm}
|
||||||
stroke="currentColor"
|
onChange={(e) => onChangeSearch(e.target.value)}
|
||||||
>
|
autoFocus
|
||||||
<circle cx="11" cy="11" r="8"></circle>
|
/>
|
||||||
<path d="m21 21-4.3-4.3"></path>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<input
|
|
||||||
type="search" className="grow"
|
|
||||||
placeholder={transI18n("searchStage")}
|
|
||||||
value={stageSearchTerm}
|
|
||||||
onChange={(e) => onChangeSearch(e.target.value)}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
|
|
||||||
</label>
|
<div className="max-h-56 overflow-y-auto space-y-1 pr-1 text-xs">
|
||||||
</div>
|
{paginatedStages.length > 0 ? (
|
||||||
|
paginatedStages.map((stage) => (
|
||||||
|
<div
|
||||||
|
key={stage.ID}
|
||||||
|
className="p-2 hover:bg-primary/20 rounded-xl cursor-pointer transition-colors"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setIsChildClick(true)
|
||||||
|
|
||||||
<div className="max-h-60 overflow-y-auto space-y-1">
|
if (extraData?.theory_craft?.stage_id !== Number(stage.ID)) {
|
||||||
{paginatedStages.length > 0 ? (
|
setExtraData({
|
||||||
<>
|
...extraData,
|
||||||
{paginatedStages.map((stage) => (
|
theory_craft: {
|
||||||
<div
|
stage_id: Number(stage.ID),
|
||||||
key={stage.ID}
|
cycle_count: extraData?.theory_craft?.cycle_count ?? 1,
|
||||||
className="p-2 hover:bg-primary/20 rounded cursor-pointer"
|
mode: extraData?.theory_craft?.mode ?? false,
|
||||||
onClick={(e) => {
|
hp: extraData?.theory_craft?.hp ?? {},
|
||||||
e.stopPropagation()
|
custom_lineup: extraData?.theory_craft?.custom_lineup ?? []
|
||||||
setIsChildClick(true)
|
|
||||||
|
|
||||||
if (extraData?.theory_craft?.stage_id !== Number(stage.ID)) {
|
|
||||||
setExtraData({
|
|
||||||
...extraData,
|
|
||||||
theory_craft: {
|
|
||||||
stage_id: Number(stage.ID),
|
|
||||||
cycle_count: extraData?.theory_craft?.cycle_count ?? 1,
|
|
||||||
mode: extraData?.theory_craft?.mode ?? false,
|
|
||||||
hp: extraData?.theory_craft?.hp ?? {},
|
|
||||||
custom_lineup: extraData?.theory_craft?.custom_lineup ?? []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
setShowSearchStage(pre => !pre)
|
})
|
||||||
onChangeSearch("")
|
}
|
||||||
}}
|
setShowSearchStage(pre => !pre)
|
||||||
>
|
onChangeSearch("")
|
||||||
{getLocaleName(locale, stage.Name)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="text-sm text-center py-4">{transI18n("noStageFound")}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{paginatedStages.length > 0 && (
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
||||||
<button
|
|
||||||
disabled={stagePage === 1}
|
|
||||||
className="btn btn-sm btn-outline btn-success mt-2"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
setStagePage(stagePage - 1)
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{transI18n("previous")}
|
{getLocaleName(locale, stage.Name)}
|
||||||
</button>
|
</div>
|
||||||
|
))
|
||||||
<button
|
) : (
|
||||||
disabled={!hasMorePages}
|
<div className="text-xs text-center py-4 text-base-content/40">{transI18n("noStageFound")}</div>
|
||||||
className="btn btn-sm btn-outline btn-success mt-2"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
setStagePage(stagePage + 1)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{transI18n("next")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
{paginatedStages.length > 0 && (
|
||||||
</label>
|
<div className="flex items-center justify-between gap-2 pt-2 border-t border-white/5">
|
||||||
</motion.div>
|
<button
|
||||||
<motion.div className="form-control bg-base-200 p-4 rounded-xl shadow relative">
|
disabled={stagePage === 1}
|
||||||
<div className="flex flex-col gap-3">
|
className="btn btn-xs btn-outline rounded-lg"
|
||||||
<label className="flex flex-wrap items-center justify-start gap-3">
|
onClick={(e) => {
|
||||||
<UsersIcon className="text-info" size={20} />
|
e.stopPropagation()
|
||||||
<span className="label-text font-semibold">{transI18n("customLineup")}</span>
|
setStagePage(stagePage - 1)
|
||||||
</label>
|
}}
|
||||||
<div className="flex flex-wrap items-center gap-2 min-h-20 p-2 bg-base-300 rounded-lg border border-base-content/10">
|
>
|
||||||
{(extraData?.theory_craft?.custom_lineup || []).length > 0 && (
|
{transI18n("previous")}
|
||||||
(extraData?.theory_craft?.custom_lineup || []).map((avatarId) => {
|
</button>
|
||||||
const avatarData = mapAvatar[avatarId];
|
<span className="text-[11px] font-mono text-base-content/50">Page {stagePage}</span>
|
||||||
if (!avatarData) return null;
|
<button
|
||||||
|
disabled={!hasMorePages}
|
||||||
|
className="btn btn-xs btn-outline rounded-lg"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setStagePage(stagePage + 1)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{transI18n("next")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Custom Lineup */}
|
||||||
|
<div className="p-3.5 rounded-xl bg-base-100/60 border border-white/5 space-y-2.5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<UsersIcon className="text-info w-4 h-4" />
|
||||||
|
<span className="text-xs sm:text-sm font-semibold">{transI18n("customLineup")}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2 min-h-16 p-2 bg-base-200/50 rounded-xl border border-white/5">
|
||||||
|
{(extraData?.theory_craft?.custom_lineup || []).map((avatarId) => {
|
||||||
|
const avatarData = mapAvatar[avatarId];
|
||||||
|
if (!avatarData) return null;
|
||||||
|
return (
|
||||||
|
<SimpleAvatarCard
|
||||||
|
key={`selected-${avatarId}`}
|
||||||
|
data={avatarData}
|
||||||
|
showRemoveHover={true}
|
||||||
|
onClick={() => {
|
||||||
|
const newLineup = (extraData?.theory_craft?.custom_lineup || []).filter(id => id !== avatarId);
|
||||||
|
setExtraData({
|
||||||
|
...extraData,
|
||||||
|
theory_craft: {
|
||||||
|
stage_id: extraData?.theory_craft?.stage_id ?? 30118121,
|
||||||
|
cycle_count: extraData?.theory_craft?.cycle_count ?? 1,
|
||||||
|
mode: extraData?.theory_craft?.mode ?? false,
|
||||||
|
hp: extraData?.theory_craft?.hp ?? {},
|
||||||
|
custom_lineup: newLineup
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="btn btn-outline btn-square border-dashed w-14 h-14 sm:w-16 sm:h-16 rounded-xl"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setShowSearchLineup(pre => !pre);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showSearchLineup && (
|
||||||
|
<div
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
className="mt-2 w-full z-50 bg-base-100 border border-base-content/15 rounded-2xl p-4 shadow-2xl space-y-3"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-center pb-2 border-b border-base-content/10">
|
||||||
|
<span className="font-bold text-xs">{transI18n("selectedCharacters")}</span>
|
||||||
|
<button className="btn btn-xs btn-circle btn-ghost" onClick={() => setShowSearchLineup(false)}>✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2 max-h-60 overflow-y-auto p-1">
|
||||||
|
{Object.values(mapAvatar).map((avatar) => {
|
||||||
|
const currentLineup = extraData?.theory_craft?.custom_lineup || [];
|
||||||
|
const isSelected = currentLineup.includes(avatar.ID.toString());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SimpleAvatarCard
|
<SimpleAvatarCard
|
||||||
key={`selected-${avatarId}`}
|
key={avatar.ID}
|
||||||
data={avatarData}
|
data={avatar}
|
||||||
showRemoveHover={true}
|
isSelected={isSelected}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const newLineup = (extraData?.theory_craft?.custom_lineup || []).filter(id => id !== avatarId);
|
const idStr = avatar.ID.toString();
|
||||||
|
let newLineup = [...currentLineup];
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
newLineup = newLineup.filter(id => id !== idStr);
|
||||||
|
} else {
|
||||||
|
newLineup.push(idStr);
|
||||||
|
}
|
||||||
|
|
||||||
setExtraData({
|
setExtraData({
|
||||||
...extraData,
|
...extraData,
|
||||||
theory_craft: {
|
theory_craft: {
|
||||||
@@ -255,80 +315,26 @@ export default function ExtraSettingBar() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})}
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="btn btn-outline btn-square border-dashed w-16 h-16 sm:w-20 sm:h-20"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowSearchLineup(pre => !pre);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 4v16m8-8H4" /></svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{showSearchLineup && (
|
|
||||||
<div
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
className="mt-2 w-full z-50 border bg-base-200 border-slate-600 rounded-lg p-4 shadow-2xl"
|
|
||||||
>
|
|
||||||
<div className="flex justify-between items-center mb-3">
|
|
||||||
<span className="font-semibold text-sm">{transI18n("selectedCharacters")}</span>
|
|
||||||
<button className="btn btn-sm btn-circle btn-error" onClick={() => setShowSearchLineup(false)}>X</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2 max-h-72 overflow-y-auto p-1">
|
|
||||||
{Object.values(mapAvatar).map((avatar) => {
|
|
||||||
const currentLineup = extraData?.theory_craft?.custom_lineup || [];
|
|
||||||
const isSelected = currentLineup.includes(avatar.ID.toString());
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SimpleAvatarCard
|
|
||||||
key={avatar.ID}
|
|
||||||
data={avatar}
|
|
||||||
isSelected={isSelected}
|
|
||||||
onClick={() => {
|
|
||||||
const idStr = avatar.ID.toString();
|
|
||||||
let newLineup = [...currentLineup];
|
|
||||||
|
|
||||||
if (isSelected) {
|
|
||||||
newLineup = newLineup.filter(id => id !== idStr);
|
|
||||||
} else {
|
|
||||||
newLineup.push(idStr);
|
|
||||||
}
|
|
||||||
|
|
||||||
setExtraData({
|
|
||||||
...extraData,
|
|
||||||
theory_craft: {
|
|
||||||
stage_id: extraData?.theory_craft?.stage_id ?? 30118121,
|
|
||||||
cycle_count: extraData?.theory_craft?.cycle_count ?? 1,
|
|
||||||
mode: extraData?.theory_craft?.mode ?? false,
|
|
||||||
hp: extraData?.theory_craft?.hp ?? {},
|
|
||||||
custom_lineup: newLineup
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
</motion.div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/*MULTIPATH CHAR */}
|
{/* MULTIPATH CHAR */}
|
||||||
<div className="space-y-4">
|
<div className="p-4 sm:p-5 rounded-2xl bg-base-200/60 border border-white/10 shadow-lg space-y-4">
|
||||||
<h3 className="text-lg font-bold flex items-center gap-2">
|
<div className="flex items-center justify-between pb-2 border-b border-base-content/10">
|
||||||
{transI18n("multipathCharacter")}
|
<div className="flex items-center gap-2">
|
||||||
<div className="tooltip tooltip-info tooltip-bottom" data-tip={transI18n("detailMultipathCharacter")}>
|
<h3 className="text-sm font-bold uppercase tracking-wider text-primary">
|
||||||
<Info className="text-primary" size={20} />
|
{transI18n("multipathCharacter")}
|
||||||
|
</h3>
|
||||||
|
<div className="tooltip tooltip-bottom tooltip-info" data-tip={transI18n("detailMultipathCharacter")}>
|
||||||
|
<Info className="text-primary w-4 h-4 cursor-pointer" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -336,87 +342,95 @@ export default function ExtraSettingBar() {
|
|||||||
checked={isEnableChangePath}
|
checked={isEnableChangePath}
|
||||||
onChange={(e) => setIsEnableChangePath(e.target.checked)}
|
onChange={(e) => setIsEnableChangePath(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</h3>
|
</div>
|
||||||
|
|
||||||
{isEnableChangePath && (
|
{isEnableChangePath && (
|
||||||
<>
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-1">
|
||||||
<motion.div className="form-control bg-base-200 p-4 rounded-xl shadow">
|
<div className="p-3.5 rounded-xl bg-base-100/60 border border-white/5 space-y-2">
|
||||||
<label className="flex items-center gap-3">
|
<div className="flex items-center gap-2">
|
||||||
<User className="text-warning" size={20} />
|
<User className="text-warning w-4 h-4" />
|
||||||
<span className="label-text font-semibold">{transI18n("mainPath")}</span>
|
<span className="text-xs font-semibold">{transI18n("mainPath")}</span>
|
||||||
<SelectCustomImage
|
</div>
|
||||||
customSet={Object.values(mapAvatar).filter((it) => extraData?.multi_path?.multi_path_main?.includes(Number(it.ID))).map((it) => ({
|
<SelectCustomImage
|
||||||
value: it.ID.toString(),
|
customSet={Object.values(mapAvatar).filter((it) => extraData?.multi_path?.multi_path_main?.includes(Number(it.ID))).map((it) => ({
|
||||||
label: getNameChar(locale, transI18n, it),
|
value: it.ID.toString(),
|
||||||
imageUrl: `${process.env.CDN_URL}/${baseType?.[it.BaseType].Icon}`
|
label: getNameChar(locale, transI18n, it),
|
||||||
}))}
|
imageUrl: `${process.env.CDN_URL}/${baseType?.[it.BaseType]?.Icon}`
|
||||||
excludeSet={[]}
|
}))}
|
||||||
selectedCustomSet={extraData?.multi_path?.main?.toString() || ""}
|
excludeSet={[]}
|
||||||
placeholder={transI18n("selectAMainStat")}
|
selectedCustomSet={extraData?.multi_path?.main?.toString() || ""}
|
||||||
setSelectedCustomSet={(it) => {
|
placeholder={transI18n("selectAMainStat")}
|
||||||
if (!it) return
|
setSelectedCustomSet={(it) => {
|
||||||
setExtraData({
|
if (!it) return
|
||||||
...extraData,
|
setExtraData({
|
||||||
multi_path: {
|
...extraData,
|
||||||
march_7: extraData?.multi_path?.march_7 || 1001,
|
multi_path: {
|
||||||
main: Number(it) || 8001,
|
march_7: extraData?.multi_path?.march_7 || 1001,
|
||||||
multi_path_main: extraData?.multi_path?.multi_path_main || [],
|
main: Number(it) || 8001,
|
||||||
multi_path_march_7: extraData?.multi_path?.multi_path_march_7 || []
|
multi_path_main: extraData?.multi_path?.multi_path_main || [],
|
||||||
},
|
multi_path_march_7: extraData?.multi_path?.multi_path_march_7 || []
|
||||||
lua: extraData?.lua || null
|
},
|
||||||
})
|
lua: extraData?.lua || null
|
||||||
}}
|
})
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
</label>
|
<div className="p-3.5 rounded-xl bg-base-100/60 border border-white/5 space-y-2">
|
||||||
</motion.div>
|
<div className="flex items-center gap-2">
|
||||||
|
<BowArrow className="text-info w-4 h-4" />
|
||||||
<motion.div className="form-control bg-base-200 p-4 rounded-xl shadow">
|
<span className="text-xs font-semibold">{transI18n("march7Path")}</span>
|
||||||
<label className="flex items-center gap-3">
|
</div>
|
||||||
<BowArrow className="text-info" size={20} />
|
<SelectCustomImage
|
||||||
<span className="label-text font-semibold">{transI18n("march7Path")}</span>
|
customSet={Object.values(mapAvatar).filter((it) => extraData?.multi_path?.multi_path_march_7?.includes(Number(it.ID))).map((it) => ({
|
||||||
<SelectCustomImage
|
value: it.ID.toString(),
|
||||||
customSet={Object.values(mapAvatar).filter((it) => extraData?.multi_path?.multi_path_march_7?.includes(Number(it.ID))).map((it) => ({
|
label: getNameChar(locale, transI18n, it),
|
||||||
value: it.ID.toString(),
|
imageUrl: `${process.env.CDN_URL}/${baseType?.[it.BaseType]?.Icon}`
|
||||||
label: getNameChar(locale, transI18n, it),
|
}))}
|
||||||
imageUrl: `${process.env.CDN_URL}/${baseType?.[it.BaseType].Icon}`
|
excludeSet={[]}
|
||||||
}))}
|
selectedCustomSet={extraData?.multi_path?.march_7?.toString() || ""}
|
||||||
excludeSet={[]}
|
placeholder={transI18n("selectAMainStat")}
|
||||||
selectedCustomSet={extraData?.multi_path?.march_7?.toString() || ""}
|
setSelectedCustomSet={(it) => {
|
||||||
placeholder={transI18n("selectAMainStat")}
|
if (!it) return
|
||||||
setSelectedCustomSet={(it) => {
|
setExtraData({
|
||||||
if (!it) return
|
...extraData,
|
||||||
setExtraData({
|
multi_path: {
|
||||||
...extraData,
|
march_7: Number(it) || 1001,
|
||||||
multi_path: {
|
main: extraData?.multi_path?.main || 8001,
|
||||||
march_7: Number(it) || 1001,
|
multi_path_main: extraData?.multi_path?.multi_path_main || [],
|
||||||
main: extraData?.multi_path?.main || 8001,
|
multi_path_march_7: extraData?.multi_path?.multi_path_march_7 || []
|
||||||
multi_path_main: extraData?.multi_path?.multi_path_main || [],
|
},
|
||||||
multi_path_march_7: extraData?.multi_path?.multi_path_march_7 || []
|
lua: extraData?.lua || null
|
||||||
},
|
})
|
||||||
lua: extraData?.lua || null
|
}}
|
||||||
})
|
/>
|
||||||
}}
|
</div>
|
||||||
/>
|
</div>
|
||||||
</label>
|
|
||||||
</motion.div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CHALLENGE */}
|
{/* CHALLENGE */}
|
||||||
<div className="space-y-4">
|
<div className="p-4 sm:p-5 rounded-2xl bg-base-200/60 border border-white/10 shadow-lg space-y-4">
|
||||||
<h3 className="text-lg font-bold">{transI18n("challenge")}</h3>
|
<div className="pb-2 border-b border-base-content/10">
|
||||||
|
<h3 className="text-sm font-bold uppercase tracking-wider text-primary">
|
||||||
|
{transI18n("challenge")}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
<motion.div className="form-control bg-base-200 p-4 rounded-xl shadow">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
<label className="flex items-center gap-3">
|
<div className="p-3.5 rounded-xl bg-base-100/60 border border-white/5 space-y-2">
|
||||||
<Swords className="text-error" size={20} />
|
<div className="flex items-center justify-between">
|
||||||
<span className="label-text font-semibold">{transI18n("anomalyArbitration")}</span>
|
<div className="flex items-center gap-2">
|
||||||
<div className="tooltip tooltip-info tooltip-bottom" data-tip={transI18n("detailChallengePeak")}>
|
<Swords className="text-error w-4 h-4" />
|
||||||
<Info className="text-primary" size={20} />
|
<span className="text-xs font-semibold">{transI18n("anomalyArbitration")}</span>
|
||||||
|
</div>
|
||||||
|
<div className="tooltip tooltip-bottom tooltip-info" data-tip={transI18n("detailChallengePeak")}>
|
||||||
|
<Info className="text-primary w-3.5 h-3.5 cursor-pointer" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
value={extraData?.challenge?.challenge_peak_group_id || ""}
|
value={extraData?.challenge?.challenge_peak_group_id || ""}
|
||||||
className="select select-error"
|
className="select select-sm select-bordered w-full rounded-xl text-xs"
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setExtraData({
|
setExtraData({
|
||||||
...extraData,
|
...extraData,
|
||||||
@@ -435,19 +449,21 @@ export default function ExtraSettingBar() {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</div>
|
||||||
</motion.div>
|
|
||||||
|
|
||||||
<motion.div className="form-control bg-base-200 p-4 rounded-xl shadow">
|
<div className="p-3.5 rounded-xl bg-base-100/60 border border-white/5 space-y-2">
|
||||||
<label className="flex items-center gap-3">
|
<div className="flex items-center justify-between">
|
||||||
<SkipForward className="text-warning" size={20} />
|
<div className="flex items-center gap-2">
|
||||||
<span className="label-text font-semibold">{transI18n("skipNode")}</span>
|
<SkipForward className="text-warning w-4 h-4" />
|
||||||
<div className="tooltip tooltip-info tooltip-bottom" data-tip={transI18n("detailSkipNode")}>
|
<span className="text-xs font-semibold">{transI18n("skipNode")}</span>
|
||||||
<Info className="text-primary" size={20} />
|
</div>
|
||||||
|
<div className="tooltip tooltip-bottom tooltip-info" data-tip={transI18n("detailSkipNode")}>
|
||||||
|
<Info className="text-primary w-3.5 h-3.5 cursor-pointer" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
value={extraData?.challenge?.skip_node || "0"}
|
value={extraData?.challenge?.skip_node || "0"}
|
||||||
className="select select-warning"
|
className="select select-sm select-bordered w-full rounded-xl text-xs"
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setExtraData({
|
setExtraData({
|
||||||
...extraData,
|
...extraData,
|
||||||
@@ -464,25 +480,35 @@ export default function ExtraSettingBar() {
|
|||||||
<option value="1">{transI18n("skipNode1")}</option>
|
<option value="1">{transI18n("skipNode1")}</option>
|
||||||
<option value="2">{transI18n("skipNode2")}</option>
|
<option value="2">{transI18n("skipNode2")}</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</div>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/*EXTRA FEATURES */}
|
{/* EXTRA FEATURES */}
|
||||||
<div className="space-y-4">
|
<div className="p-4 sm:p-5 rounded-2xl bg-base-200/60 border border-white/10 shadow-lg space-y-4">
|
||||||
<h3 className="text-lg font-bold">{transI18n("extraFeatures")}</h3>
|
<div className="pb-2 border-b border-base-content/10">
|
||||||
|
<h3 className="text-sm font-bold uppercase tracking-wider text-primary">
|
||||||
|
{transI18n("extraFeatures")}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
{extraData?.setting?.hide_ui !== undefined && (
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
<motion.div className="form-control bg-base-200 p-4 rounded-xl shadow">
|
{extraData?.setting?.hide_ui !== undefined && (
|
||||||
<label className="flex flex-wrap items-center cursor-pointer justify-start gap-3">
|
<div className="p-3.5 rounded-xl bg-base-100/60 border border-white/5 flex items-center justify-between">
|
||||||
{extraData?.setting?.hide_ui
|
<div className="flex items-center gap-2.5">
|
||||||
? <EyeOff className="text-warning" size={20} />
|
{extraData?.setting?.hide_ui
|
||||||
: <Eye className="text-success" size={20} />
|
? <EyeOff className="text-warning w-4 h-4" />
|
||||||
}
|
: <Eye className="text-success w-4 h-4" />
|
||||||
|
}
|
||||||
|
<span className="text-xs font-semibold">{transI18n("hideUI")}</span>
|
||||||
|
<div className="tooltip tooltip-bottom tooltip-info" data-tip={transI18n("detailHiddenUi")}>
|
||||||
|
<Info className="text-primary w-3.5 h-3.5 cursor-pointer" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="toggle toggle-secondary"
|
className="toggle toggle-primary toggle-sm"
|
||||||
checked={extraData?.setting?.hide_ui}
|
checked={extraData?.setting?.hide_ui}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setExtraData({
|
setExtraData({
|
||||||
@@ -496,21 +522,22 @@ export default function ExtraSettingBar() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span className="label-text font-semibold">{transI18n("hideUI")}</span>
|
</div>
|
||||||
<div className="tooltip tooltip-info tooltip-bottom" data-tip={transI18n("detailHiddenUi")}>
|
)}
|
||||||
<Info className="text-primary" size={20} />
|
|
||||||
|
{extraData?.setting?.censorship !== undefined && (
|
||||||
|
<div className="p-3.5 rounded-xl bg-base-100/60 border border-white/5 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<ShieldBan className="text-error w-4 h-4" />
|
||||||
|
<span className="text-xs font-semibold">{transI18n("disableCensorship")}</span>
|
||||||
|
<div className="tooltip tooltip-bottom tooltip-info" data-tip={transI18n("detailDisableCensorship")}>
|
||||||
|
<Info className="text-primary w-3.5 h-3.5 cursor-pointer" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{extraData?.setting?.censorship !== undefined && (
|
|
||||||
<motion.div className="form-control bg-base-200 p-4 rounded-xl shadow">
|
|
||||||
<label className="flex flex-wrap items-center cursor-pointer justify-start gap-3">
|
|
||||||
<ShieldBan className="text-error" size={20} />
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="toggle toggle-accent"
|
className="toggle toggle-accent toggle-sm"
|
||||||
checked={extraData?.setting?.censorship}
|
checked={extraData?.setting?.censorship}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setExtraData({
|
setExtraData({
|
||||||
@@ -524,20 +551,17 @@ export default function ExtraSettingBar() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<span className="label-text font-semibold">{transI18n("disableCensorship")}</span>
|
</div>
|
||||||
<div className="tooltip tooltip-info tooltip-bottom" data-tip={transI18n("detailDisableCensorship")}>
|
)}
|
||||||
<Info className="text-primary" size={20} />
|
</div>
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
{/* LUA EDITOR */}
|
||||||
<h3 className="text-lg font-bold flex items-center gap-2">
|
<div className="p-4 sm:p-5 rounded-2xl bg-base-200/60 border border-white/10 shadow-lg space-y-4">
|
||||||
{"Lua"}
|
<div className="flex items-center justify-between pb-2 border-b border-base-content/10">
|
||||||
<div className="tooltip tooltip-info tooltip-bottom">
|
<div className="flex items-center gap-2">
|
||||||
<CupSoda className="text-primary" size={20} />
|
<CupSoda className="text-primary w-4 h-4" />
|
||||||
|
<h3 className="text-sm font-bold uppercase tracking-wider text-primary">Lua Scripting</h3>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -545,40 +569,30 @@ export default function ExtraSettingBar() {
|
|||||||
checked={isEnableLua}
|
checked={isEnableLua}
|
||||||
onChange={(e) => setIsEnableLua(e.target.checked)}
|
onChange={(e) => setIsEnableLua(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
</h3>
|
</div>
|
||||||
{isEnableLua && (
|
|
||||||
<>
|
|
||||||
<div className="form-control bg-base-200 p-4 rounded-xl shadow">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text">Lua Script</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className="rounded-xl overflow-hidden border border-base-300">
|
{isEnableLua && (
|
||||||
<Editor
|
<div className="rounded-2xl overflow-hidden border border-white/10 shadow-inner">
|
||||||
value={extraData?.lua || ""}
|
<Editor
|
||||||
onValueChange={(code) =>
|
value={extraData?.lua || ""}
|
||||||
setExtraData(
|
onValueChange={(code) =>
|
||||||
{
|
setExtraData({
|
||||||
...extraData,
|
...extraData,
|
||||||
lua: code
|
lua: code
|
||||||
}
|
})
|
||||||
)
|
}
|
||||||
}
|
highlight={(code) => Prism.highlight(code, Prism.languages.lua, "lua")}
|
||||||
highlight={(code) => Prism.highlight(code, Prism.languages.lua, "lua")}
|
padding={16}
|
||||||
padding={16}
|
textareaClassName="outline-none font-mono"
|
||||||
textareaClassName="outline-none"
|
className="text-xs font-mono min-h-60 bg-[#161822] text-white"
|
||||||
className="text-sm font-mono min-h-75"
|
style={{
|
||||||
style={{
|
background: "#161822",
|
||||||
background: "#1e1e1e",
|
color: "#e2e8f0",
|
||||||
color: "#fff",
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10">
|
<footer className="w-full border-t border-base-content/10 bg-base-100 py-6 px-4 mt-auto">
|
||||||
<aside>
|
<div className="max-w-[1920px] mx-auto flex flex-col sm:flex-row items-center justify-between gap-4 text-xs text-base-content/60 font-medium">
|
||||||
<p>Copyright © {new Date().getFullYear()} - Firefly Shelter (Powered by Nextjs & DaisyUi)</p>
|
<div className="flex items-center gap-2">
|
||||||
</aside>
|
<span className="font-semibold text-base-content/80">Firefly Shelter</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>S Game Toolkit</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span>© {new Date().getFullYear()}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span className="px-2 py-0.5 rounded-full bg-base-200 text-base-content/70 border border-white/5">Next.js & DaisyUI</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+314
-274
@@ -6,7 +6,6 @@ import { useChangeTheme } from "@/hooks/useChangeTheme";
|
|||||||
import { listCurrentLanguage, listCurrentLanguageApi } from "@/constant/constant";
|
import { listCurrentLanguage, listCurrentLanguageApi } from "@/constant/constant";
|
||||||
import useLocaleStore from "@/stores/localeStore";
|
import useLocaleStore from "@/stores/localeStore";
|
||||||
import useUserDataStore from "@/stores/userDataStore";
|
import useUserDataStore from "@/stores/userDataStore";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
@@ -23,6 +22,7 @@ import ExtraSettingBar from "../extraSettingBar";
|
|||||||
import ChangelogBar from "../changelog";
|
import ChangelogBar from "../changelog";
|
||||||
import { ModalConfig } from "@/types";
|
import { ModalConfig } from "@/types";
|
||||||
import { ScrollText } from "lucide-react";
|
import { ScrollText } from "lucide-react";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
|
||||||
const themes = [
|
const themes = [
|
||||||
{ label: "Winter" },
|
{ label: "Winter" },
|
||||||
@@ -33,7 +33,12 @@ const themes = [
|
|||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const { changeTheme } = useChangeTheme()
|
const { changeTheme } = useChangeTheme()
|
||||||
const { locale, setLocale } = useLocaleStore()
|
const { locale, setLocale } = useLocaleStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
locale: state.locale,
|
||||||
|
setLocale: state.setLocale,
|
||||||
|
}))
|
||||||
|
)
|
||||||
const {
|
const {
|
||||||
avatars,
|
avatars,
|
||||||
battle_type,
|
battle_type,
|
||||||
@@ -49,7 +54,24 @@ export default function Header() {
|
|||||||
setAsConfig,
|
setAsConfig,
|
||||||
setCeConfig,
|
setCeConfig,
|
||||||
setPeakConfig,
|
setPeakConfig,
|
||||||
} = useUserDataStore()
|
} = useUserDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
avatars: state.avatars,
|
||||||
|
battle_type: state.battle_type,
|
||||||
|
setAvatars: state.setAvatars,
|
||||||
|
setBattleType: state.setBattleType,
|
||||||
|
moc_config: state.moc_config,
|
||||||
|
pf_config: state.pf_config,
|
||||||
|
as_config: state.as_config,
|
||||||
|
ce_config: state.ce_config,
|
||||||
|
peak_config: state.peak_config,
|
||||||
|
setMocConfig: state.setMocConfig,
|
||||||
|
setPfConfig: state.setPfConfig,
|
||||||
|
setAsConfig: state.setAsConfig,
|
||||||
|
setCeConfig: state.setCeConfig,
|
||||||
|
setPeakConfig: state.setPeakConfig,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
@@ -64,11 +86,29 @@ export default function Header() {
|
|||||||
isOpenExtra,
|
isOpenExtra,
|
||||||
setIsChangelog,
|
setIsChangelog,
|
||||||
isChangelog
|
isChangelog
|
||||||
} = useModelStore()
|
} = useModelStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
setIsOpenImport: state.setIsOpenImport,
|
||||||
|
isOpenImport: state.isOpenImport,
|
||||||
|
setIsOpenMonster: state.setIsOpenMonster,
|
||||||
|
isOpenMonster: state.isOpenMonster,
|
||||||
|
setIsOpenConnect: state.setIsOpenConnect,
|
||||||
|
isOpenConnect: state.isOpenConnect,
|
||||||
|
setIsOpenExtra: state.setIsOpenExtra,
|
||||||
|
isOpenExtra: state.isOpenExtra,
|
||||||
|
setIsChangelog: state.setIsChangelog,
|
||||||
|
isChangelog: state.isChangelog,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
const [importModal, setImportModal] = useState("enka");
|
const [importModal, setImportModal] = useState("enka");
|
||||||
|
|
||||||
const { isConnectPS, extraData } = useGlobalStore()
|
const { isConnectPS, extraData } = useGlobalStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
isConnectPS: state.isConnectPS,
|
||||||
|
extraData: state.extraData,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
@@ -234,24 +274,149 @@ export default function Header() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="navbar bg-base-100 shadow-md sticky top-0 z-50 px-3 py-1">
|
<header className="sticky top-0 z-40 w-full bg-base-100 border-b border-base-300 shadow-xs">
|
||||||
<div className="navbar-start">
|
<div className="navbar max-w-[1920px] mx-auto px-2 sm:px-4 py-1.5 min-h-14">
|
||||||
{/* Mobile menu dropdown */}
|
<div className="navbar-start gap-2">
|
||||||
<div className="dropdown">
|
{/* Mobile menu dropdown */}
|
||||||
<div tabIndex={0} role="button" className="btn btn-ghost btn-sm lg:hidden hover:bg-base-200 transition-all duration-300">
|
<div className="dropdown">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<div tabIndex={0} role="button" className="btn btn-ghost btn-sm lg:hidden rounded-lg hover:bg-base-200 transition-all duration-200">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h8m-8 6h16" />
|
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
</svg>
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h8m-8 6h16" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<ul
|
||||||
|
tabIndex={0}
|
||||||
|
className="menu menu-sm dropdown-content bg-base-100 rounded-2xl z-50 mt-3 w-56 p-2 shadow-2xl border border-base-content/15"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<details>
|
||||||
|
<summary className="px-3 py-2 hover:bg-base-200/70 rounded-lg transition-all duration-200 font-medium w-full">{transI18n("loadData")}</summary>
|
||||||
|
<ul className="p-2 space-y-1">
|
||||||
|
<li><a className="rounded-md" onClick={() => {
|
||||||
|
setImportModal("freesr")
|
||||||
|
setIsOpenImport(true)
|
||||||
|
}}>{transI18n("freeSr")}</a></li>
|
||||||
|
<li>
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="application/json"
|
||||||
|
id="database-data-upload"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleImportDatabase}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="rounded-md w-full text-left"
|
||||||
|
onClick={() => document.getElementById('database-data-upload')?.click()}
|
||||||
|
>
|
||||||
|
{transI18n("database")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
</li>
|
||||||
|
<li><a className="rounded-md" onClick={() => {
|
||||||
|
setImportModal("enka")
|
||||||
|
setIsOpenImport(true)
|
||||||
|
}}>{transI18n("enka")}</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<details>
|
||||||
|
<summary className="px-3 py-2 hover:bg-base-200/70 rounded-lg transition-all duration-200 font-medium">{transI18n("exportData")}</summary>
|
||||||
|
<ul className="p-2 space-y-1">
|
||||||
|
<li><a className="rounded-md" onClick={() => downloadJson("freesr-data",
|
||||||
|
converterToFreeSRJson(
|
||||||
|
avatars,
|
||||||
|
battle_type,
|
||||||
|
moc_config,
|
||||||
|
pf_config,
|
||||||
|
as_config,
|
||||||
|
ce_config,
|
||||||
|
peak_config
|
||||||
|
)
|
||||||
|
)}>{transI18n("freeSr")}</a></li>
|
||||||
|
<li><a className="rounded-md" onClick={() => downloadJson("database-data", {
|
||||||
|
avatars: avatars,
|
||||||
|
battle_type: battle_type,
|
||||||
|
moc_config: moc_config,
|
||||||
|
pf_config: pf_config,
|
||||||
|
as_config: as_config,
|
||||||
|
ce_config: ce_config,
|
||||||
|
peak_config: peak_config
|
||||||
|
})}>{transI18n("database")}</a></li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
className="px-3 py-2 hover:bg-base-200/70 rounded-lg transition-all duration-200 font-medium"
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpenConnect(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{transI18n("connectSetting")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpenMonster(true)
|
||||||
|
}}
|
||||||
|
className="px-3 py-2 hover:bg-base-200/70 rounded-lg transition-all duration-200 font-medium"
|
||||||
|
>
|
||||||
|
{transI18n("monsterSetting")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{extraData && (
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsOpenExtra(true)
|
||||||
|
}}
|
||||||
|
className="px-3 py-2 hover:bg-base-200/70 rounded-lg transition-all duration-200 font-medium"
|
||||||
|
>
|
||||||
|
{transI18n("extraSetting")}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<ul
|
|
||||||
tabIndex={0}
|
{/* Brand Logo */}
|
||||||
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-10 mt-3 w-52 p-2 shadow-md border border-base-200"
|
<a className="flex items-center gap-2.5 px-2 group cursor-pointer select-none">
|
||||||
>
|
<div className="relative">
|
||||||
|
<div className="absolute -inset-1 bg-linear-to-r from-emerald-500/20 to-orange-500/20 rounded-full blur-xs opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
||||||
|
<Image
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src="/ff-srtool.png"
|
||||||
|
alt="Logo"
|
||||||
|
width={250}
|
||||||
|
height={250}
|
||||||
|
className="relative w-9 h-9 xl:w-10 xl:h-10 object-contain drop-shadow-sm transition-transform duration-300 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-center items-start">
|
||||||
|
<h1 className="text-base xl:text-lg font-bold leading-tight tracking-tight flex items-center gap-1">
|
||||||
|
<span className="text-primary font-extrabold">Firefly</span>
|
||||||
|
<span className="bg-clip-text text-transparent bg-linear-to-r from-emerald-400 via-amber-400 to-rose-400">
|
||||||
|
SrTools
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-[11px] text-base-content/50 font-medium tracking-wide uppercase">Firefly Shelter</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop navigation */}
|
||||||
|
<div className="navbar-center hidden lg:flex">
|
||||||
|
<ul className="menu menu-horizontal gap-1.5 p-0 text-sm font-medium">
|
||||||
<li>
|
<li>
|
||||||
<details>
|
<details>
|
||||||
<summary className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium w-full">{transI18n("loadData")}</summary>
|
<summary className="px-3 py-1.5 hover:bg-base-200/70 rounded-lg transition-all duration-200">{transI18n("loadData")}</summary>
|
||||||
<ul className="p-2">
|
<ul className="p-2 bg-base-100 rounded-xl shadow-xl border border-base-content/15 space-y-1 w-44 z-50">
|
||||||
<li><a onClick={() => {
|
<li><a className="rounded-md" onClick={() => {
|
||||||
setImportModal("freesr")
|
setImportModal("freesr")
|
||||||
setIsOpenImport(true)
|
setIsOpenImport(true)
|
||||||
}}>{transI18n("freeSr")}</a></li>
|
}}>{transI18n("freeSr")}</a></li>
|
||||||
@@ -265,13 +430,14 @@ export default function Header() {
|
|||||||
onChange={handleImportDatabase}
|
onChange={handleImportDatabase}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
|
className="rounded-md w-full text-left"
|
||||||
onClick={() => document.getElementById('database-data-upload')?.click()}
|
onClick={() => document.getElementById('database-data-upload')?.click()}
|
||||||
>
|
>
|
||||||
{transI18n("database")}
|
{transI18n("database")}
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
</li>
|
</li>
|
||||||
<li><a onClick={() => {
|
<li><a className="rounded-md" onClick={() => {
|
||||||
setImportModal("enka")
|
setImportModal("enka")
|
||||||
setIsOpenImport(true)
|
setIsOpenImport(true)
|
||||||
}}>{transI18n("enka")}</a></li>
|
}}>{transI18n("enka")}</a></li>
|
||||||
@@ -280,9 +446,9 @@ export default function Header() {
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<details>
|
<details>
|
||||||
<summary className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium">{transI18n("exportData")}</summary>
|
<summary className="px-3 py-1.5 hover:bg-base-200/70 rounded-lg transition-all duration-200">{transI18n("exportData")}</summary>
|
||||||
<ul className="p-2">
|
<ul className="p-2 bg-base-100 rounded-xl shadow-xl border border-base-content/15 space-y-1 w-44 z-50">
|
||||||
<li><a onClick={() => downloadJson("freesr-data",
|
<li><a className="rounded-md" onClick={() => downloadJson("freesr-data",
|
||||||
converterToFreeSRJson(
|
converterToFreeSRJson(
|
||||||
avatars,
|
avatars,
|
||||||
battle_type,
|
battle_type,
|
||||||
@@ -293,7 +459,7 @@ export default function Header() {
|
|||||||
peak_config
|
peak_config
|
||||||
)
|
)
|
||||||
)}>{transI18n("freeSr")}</a></li>
|
)}>{transI18n("freeSr")}</a></li>
|
||||||
<li><a onClick={() => downloadJson("database-data", {
|
<li><a className="rounded-md" onClick={() => downloadJson("database-data", {
|
||||||
avatars: avatars,
|
avatars: avatars,
|
||||||
battle_type: battle_type,
|
battle_type: battle_type,
|
||||||
moc_config: moc_config,
|
moc_config: moc_config,
|
||||||
@@ -307,7 +473,7 @@ export default function Header() {
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
className="px-3 py-1.5 hover:bg-base-200/70 rounded-lg transition-all duration-200"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOpenConnect(true)
|
setIsOpenConnect(true)
|
||||||
}}
|
}}
|
||||||
@@ -320,19 +486,18 @@ export default function Header() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOpenMonster(true)
|
setIsOpenMonster(true)
|
||||||
}}
|
}}
|
||||||
className="disabled px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
className="px-3 py-1.5 hover:bg-base-200/70 rounded-lg transition-all duration-200"
|
||||||
>
|
>
|
||||||
{transI18n("monsterSetting")}
|
{transI18n("monsterSetting")}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{extraData && (
|
{extraData && (
|
||||||
<li>
|
<li>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsOpenExtra(true)
|
setIsOpenExtra(true)
|
||||||
}}
|
}}
|
||||||
className="disabled px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
className="px-3 py-1.5 hover:bg-base-200/70 rounded-lg transition-all duration-200"
|
||||||
>
|
>
|
||||||
{transI18n("extraSetting")}
|
{transI18n("extraSetting")}
|
||||||
</button>
|
</button>
|
||||||
@@ -341,279 +506,154 @@ export default function Header() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Logo */}
|
{/* Right side items */}
|
||||||
|
<div className="navbar-end gap-2 pr-1">
|
||||||
<a className="hidden sm:grid sm:grid-cols-1 items-start justify-items-center text-left gap-0 hover:scale-105 px-2">
|
{/* PS Connection Status Badge */}
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div data-tip={isConnectPS ? "Connected to PS" : "Not Connected"} className="tooltip tooltip-bottom">
|
||||||
<Image
|
<div className="flex items-center gap-2 px-2.5 py-1 rounded-full bg-base-200/60 border border-white/5 shadow-xs">
|
||||||
unoptimized
|
<span className="relative flex h-2 w-2">
|
||||||
crossOrigin="anonymous"
|
{isConnectPS && (
|
||||||
src="/ff-srtool.png"
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||||
alt="Logo"
|
)}
|
||||||
width={250}
|
<span className={`relative inline-flex rounded-full h-2 w-2 ${isConnectPS ? 'bg-emerald-500 shadow-[0_0_8px_rgba(16,185,129,0.8)]' : 'bg-rose-500'}`}></span>
|
||||||
height={250}
|
</span>
|
||||||
className="w-10 h-10 xl:w-12 xl:h-12 object-contain"
|
<span className={`hidden xl:block text-xs font-semibold tracking-tight ${isConnectPS ? 'text-emerald-500' : 'text-base-content/50'}`}>
|
||||||
/>
|
{isConnectPS ? transI18n("connected") : transI18n("unconnected")}
|
||||||
<div className="flex flex-col justify-center items-start">
|
</span>
|
||||||
<h1 className="text-lg xl:text-xl font-bold leading-tight">
|
|
||||||
<span className="text-emerald-500">Firefly Sr</span>
|
|
||||||
<span className="bg-clip-text text-transparent bg-linear-to-r from-emerald-400 via-orange-500 to-red-500">
|
|
||||||
Tools
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
<p className="text-sm text-gray-500">Firefly Shelter</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
|
||||||
|
|
||||||
</div>
|
{/* View Logs Button */}
|
||||||
|
<button
|
||||||
|
data-tip="Changelog"
|
||||||
|
title="Changelog"
|
||||||
|
className="btn btn-ghost btn-sm btn-circle tooltip tooltip-bottom text-warning/90 hover:bg-warning/10 transition-colors"
|
||||||
|
onClick={() => {
|
||||||
|
setIsChangelog(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ScrollText className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
{/* Desktop navigation */}
|
{/* Language selector */}
|
||||||
<div className="navbar-center hidden lg:flex">
|
<div className="dropdown dropdown-end tooltip tooltip-bottom" data-tip={transI18n("language") || "Language"}>
|
||||||
<ul className="menu menu-horizontal gap-1">
|
<div
|
||||||
<li>
|
tabIndex={0}
|
||||||
<details>
|
role="button"
|
||||||
<summary className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium">{transI18n("loadData")}</summary>
|
className="flex items-center gap-1.5 border border-base-content/10 bg-base-200/40 rounded-lg text-xs px-2.5 py-1 hover:bg-base-200/80 transition-all duration-200 font-semibold uppercase tracking-wider"
|
||||||
<ul className="p-2">
|
|
||||||
<li><a onClick={() => {
|
|
||||||
setImportModal("freesr")
|
|
||||||
setIsOpenImport(true)
|
|
||||||
}}>{transI18n("freeSr")}</a></li>
|
|
||||||
<li>
|
|
||||||
<>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept="application/json"
|
|
||||||
id="database-data-upload"
|
|
||||||
className="hidden"
|
|
||||||
onChange={handleImportDatabase}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
onClick={() => document.getElementById('database-data-upload')?.click()}
|
|
||||||
>
|
|
||||||
{transI18n("database")}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
</li>
|
|
||||||
<li><a onClick={() => {
|
|
||||||
setImportModal("enka")
|
|
||||||
setIsOpenImport(true)
|
|
||||||
}}>{transI18n("enka")}</a></li>
|
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<details>
|
|
||||||
<summary className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium">{transI18n("exportData")}</summary>
|
|
||||||
<ul className="p-2">
|
|
||||||
<li><a onClick={() => downloadJson("freesr-data",
|
|
||||||
converterToFreeSRJson(
|
|
||||||
avatars,
|
|
||||||
battle_type,
|
|
||||||
moc_config,
|
|
||||||
pf_config,
|
|
||||||
as_config,
|
|
||||||
ce_config,
|
|
||||||
peak_config
|
|
||||||
)
|
|
||||||
)}>{transI18n("freeSr")}</a></li>
|
|
||||||
<li><a onClick={() => downloadJson("database-data", {
|
|
||||||
avatars: avatars,
|
|
||||||
battle_type: battle_type,
|
|
||||||
moc_config: moc_config,
|
|
||||||
pf_config: pf_config,
|
|
||||||
as_config: as_config,
|
|
||||||
ce_config: ce_config,
|
|
||||||
peak_config: peak_config
|
|
||||||
})}>{transI18n("database")}</a></li>
|
|
||||||
</ul>
|
|
||||||
</details>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
|
||||||
onClick={() => {
|
|
||||||
setIsOpenConnect(true)
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{transI18n("connectSetting")}
|
<span className="font-bold">{listCurrentLanguage[locale as keyof typeof listCurrentLanguage]?.flag || "EN"}</span>
|
||||||
</button>
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="size-3.5 opacity-60">
|
||||||
</li>
|
<path fillRule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clipRule="evenodd" />
|
||||||
<li>
|
</svg>
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setIsOpenMonster(true)
|
|
||||||
}}
|
|
||||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
|
||||||
>
|
|
||||||
{transI18n("monsterSetting")}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{extraData && (
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setIsOpenExtra(true)
|
|
||||||
}}
|
|
||||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
|
||||||
>
|
|
||||||
{transI18n("extraSetting")}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Right side items */}
|
|
||||||
<div className="navbar-end gap-2 pr-1 ">
|
|
||||||
<div data-tip="Connection Status" className="tooltip tooltip-bottom">
|
|
||||||
<div className="flex items-center space-x-2 p-1.5 rounded-full shadow-md">
|
|
||||||
<div className={`hidden xl:block text-sm italic ${isConnectPS ? 'text-green-500' : 'text-red-500'}`}>
|
|
||||||
{isConnectPS ? transI18n("connected") : transI18n("unconnected")}
|
|
||||||
</div>
|
</div>
|
||||||
<div className={`w-3 h-3 rounded-full ${isConnectPS ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<ul
|
||||||
data-tip="View Logs"
|
tabIndex={0}
|
||||||
className="btn btn-ghost btn-sm btn-circle items-center justify-center w-fit tooltip tooltip-bottom"
|
className="dropdown-content z-50 menu p-1.5 shadow-2xl bg-base-100 rounded-xl w-48 mt-2 border border-base-content/15 space-y-0.5"
|
||||||
onClick={() => {
|
|
||||||
setIsChangelog(true)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ScrollText className="w-5 h-5 text-warning" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Language selector - REFINED */}
|
|
||||||
<div className="dropdown dropdown-end">
|
|
||||||
{/* Nút bấm hiển thị */}
|
|
||||||
<div
|
|
||||||
tabIndex={0}
|
|
||||||
role="button"
|
|
||||||
className="flex items-center gap-1 border border-base-300 rounded text-sm px-2 py-0.5 hover:bg-base-200 transition-all duration-200 uppercase font-medium"
|
|
||||||
>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-4">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802" />
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
{listCurrentLanguage[locale as keyof typeof listCurrentLanguage]?.flag || "EN"}
|
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="size-4 opacity-50">
|
|
||||||
<path fillRule="evenodd" d="M5.22 8.22a.75.75 0 0 1 1.06 0L10 11.94l3.72-3.72a.75.75 0 1 1 1.06 1.06l-4.25 4.25a.75.75 0 0 1-1.06 0L5.22 9.28a.75.75 0 0 1 0-1.06Z" clipRule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul
|
|
||||||
tabIndex={0}
|
|
||||||
className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box w-52 mt-2"
|
|
||||||
>
|
|
||||||
{Object.entries(listCurrentLanguage).map(([key, value]) => (
|
|
||||||
<li key={key}>
|
|
||||||
<button
|
|
||||||
className={`flex justify-between ${locale === key ? "active" : ""}`}
|
|
||||||
onClick={() => {
|
|
||||||
changeLocale(key);
|
|
||||||
const elem = document.activeElement;
|
|
||||||
if (elem instanceof HTMLElement) {
|
|
||||||
elem.blur();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="font-bold">{value.flag}</span>
|
|
||||||
<span>{value.label}</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className="dropdown dropdown-end">
|
|
||||||
<div tabIndex={0} role="button" className="btn btn-ghost btn-sm hover:bg-base-200 transition-all duration-200 px-2">
|
|
||||||
<svg
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
className="h-4 w-4 stroke-current"
|
|
||||||
>
|
>
|
||||||
<path
|
{Object.entries(listCurrentLanguage).map(([key, value]) => (
|
||||||
strokeLinecap="round"
|
<li key={key}>
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth={2}
|
|
||||||
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<svg
|
|
||||||
width="10px"
|
|
||||||
height="10px"
|
|
||||||
className="ml-1 h-2 w-2 fill-current opacity-60"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 2048 2048"
|
|
||||||
>
|
|
||||||
<path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
tabIndex={0}
|
|
||||||
className="dropdown-content bg-base-100 text-base-content rounded-lg top-px overflow-y-auto border border-base-200 shadow-md mt-12"
|
|
||||||
>
|
|
||||||
<ul className="menu w-40 p-1">
|
|
||||||
{themes.map((theme) => (
|
|
||||||
<li
|
|
||||||
key={theme.label}
|
|
||||||
onClick={() => {
|
|
||||||
if (changeTheme) changeTheme(theme.label.toLowerCase());
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
className="gap-2 px-2 py-1.5 hover:bg-base-200 rounded text-sm transition-all duration-200"
|
className={`flex justify-between rounded-lg py-1.5 text-xs font-medium ${locale === key ? "active bg-primary/20 text-primary font-bold" : "hover:bg-base-200/70"}`}
|
||||||
data-set-theme={theme.label.toLowerCase()}
|
onClick={() => {
|
||||||
data-act-class="[&_svg]:visible"
|
changeLocale(key);
|
||||||
|
const elem = document.activeElement;
|
||||||
|
if (elem instanceof HTMLElement) {
|
||||||
|
elem.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<span className="font-bold">{value.flag}</span>
|
||||||
data-theme={theme.label.toLowerCase()}
|
<span>{value.label}</span>
|
||||||
className="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded p-1 shadow-sm"
|
|
||||||
>
|
|
||||||
<div className="bg-base-content size-1 rounded-full" />
|
|
||||||
<div className="bg-primary size-1 rounded-full" />
|
|
||||||
<div className="bg-secondary size-1 rounded-full" />
|
|
||||||
<div className="bg-accent size-1 rounded-full" />
|
|
||||||
</div>
|
|
||||||
<div className="text-sm">{theme.label}</div>
|
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Theme selector */}
|
||||||
|
<div className="dropdown dropdown-end tooltip tooltip-bottom" data-tip="Theme">
|
||||||
|
<div tabIndex={0} role="button" className="btn btn-ghost btn-sm rounded-lg hover:bg-base-200/70 transition-all duration-200 px-2" title="Theme">
|
||||||
|
<svg
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
className="h-4 w-4 stroke-current"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
tabIndex={0}
|
||||||
|
className="dropdown-content bg-base-100 text-base-content rounded-xl overflow-hidden border border-base-content/15 shadow-2xl mt-2 z-50"
|
||||||
|
>
|
||||||
|
<ul className="menu w-36 p-1.5 space-y-0.5">
|
||||||
|
{themes.map((theme) => (
|
||||||
|
<li
|
||||||
|
key={theme.label}
|
||||||
|
onClick={() => {
|
||||||
|
if (changeTheme) changeTheme(theme.label.toLowerCase());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="flex items-center gap-2 px-2.5 py-1.5 hover:bg-base-200/70 rounded-lg text-xs font-medium transition-all duration-200"
|
||||||
|
data-set-theme={theme.label.toLowerCase()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
data-theme={theme.label.toLowerCase()}
|
||||||
|
className="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded p-1 shadow-xs border border-white/10"
|
||||||
|
>
|
||||||
|
<div className="bg-base-content size-1 rounded-full" />
|
||||||
|
<div className="bg-primary size-1 rounded-full" />
|
||||||
|
<div className="bg-secondary size-1 rounded-full" />
|
||||||
|
<div className="bg-accent size-1 rounded-full" />
|
||||||
|
</div>
|
||||||
|
<span>{theme.label}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Modals */}
|
||||||
{modalConfigs?.map(({ id, title, onClose, content }) => (
|
{modalConfigs?.map(({ id, title, onClose, content }) => (
|
||||||
<dialog key={id} id={id} className="modal">
|
<dialog key={id} id={id} className="modal">
|
||||||
<div className="modal-box w-11/12 max-w-[90%] max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
|
<div className={`modal-box w-11/12 max-w-[92vw] sm:max-w-[88vw] ${id === 'monster_modal' ? 'h-[85vh] max-h-[85vh] flex flex-col' : 'max-h-[90vh]'} bg-base-100 text-base-content border border-base-content/10 shadow-2xl rounded-2xl p-0 overflow-hidden`}>
|
||||||
<div className="sticky top-0 z-10">
|
{/* Modal Header */}
|
||||||
<motion.button
|
<div className="sticky top-0 z-20 flex items-center justify-between px-6 py-4 border-b border-base-content/10 bg-base-100 shrink-0">
|
||||||
whileHover={{ scale: 1.1, rotate: 90 }}
|
<h3 className="font-bold text-xl tracking-tight bg-clip-text text-transparent bg-linear-to-r from-primary via-accent to-secondary">
|
||||||
transition={{ duration: 0.2 }}
|
{title}
|
||||||
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
|
</h3>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-circle btn-ghost text-base-content/60 hover:text-base-content hover:bg-base-200 transition-colors"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</motion.button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
{/* Modal Body */}
|
||||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
<div className="p-4 sm:p-6 overflow-y-auto flex-1">
|
||||||
{title}
|
{content}
|
||||||
</h3>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{content}
|
|
||||||
</div>
|
</div>
|
||||||
|
<form method="dialog" className="modal-backdrop">
|
||||||
|
<button onClick={onClose}>close</button>
|
||||||
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
))}
|
))}
|
||||||
</div>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+178
-121
@@ -11,12 +11,32 @@ import { useTranslations } from "next-intl";
|
|||||||
import SelectCustomImage from "../select/customSelectImage";
|
import SelectCustomImage from "../select/customSelectImage";
|
||||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||||
import useDetailDataStore from "@/stores/detailDataStore";
|
import useDetailDataStore from "@/stores/detailDataStore";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
|
||||||
export default function CopyImport() {
|
export default function CopyImport() {
|
||||||
const { avatars, setAvatar } = useUserDataStore();
|
const { avatars, setAvatar } = useUserDataStore(
|
||||||
const { avatarSelected } = useCurrentDataStore()
|
useShallow((state) => ({
|
||||||
const { mapAvatar, baseType, damageType } = useDetailDataStore()
|
avatars: state.avatars,
|
||||||
const { locale } = useLocaleStore()
|
setAvatar: state.setAvatar,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const { avatarSelected } = useCurrentDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
avatarSelected: state.avatarSelected,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const { mapAvatar, baseType, damageType } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
mapAvatar: state.mapAvatar,
|
||||||
|
baseType: state.baseType,
|
||||||
|
damageType: state.damageType,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const { locale } = useLocaleStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
locale: state.locale,
|
||||||
|
}))
|
||||||
|
);
|
||||||
const {
|
const {
|
||||||
selectedProfiles,
|
selectedProfiles,
|
||||||
avatarCopySelected,
|
avatarCopySelected,
|
||||||
@@ -28,7 +48,20 @@ export default function CopyImport() {
|
|||||||
setListElement,
|
setListElement,
|
||||||
setListPath,
|
setListPath,
|
||||||
setListRank
|
setListRank
|
||||||
} = useCopyProfileStore()
|
} = useCopyProfileStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
selectedProfiles: state.selectedProfiles,
|
||||||
|
avatarCopySelected: state.avatarCopySelected,
|
||||||
|
setSelectedProfiles: state.setSelectedProfiles,
|
||||||
|
setAvatarCopySelected: state.setAvatarCopySelected,
|
||||||
|
listElement: state.listElement,
|
||||||
|
listPath: state.listPath,
|
||||||
|
listRank: state.listRank,
|
||||||
|
setListElement: state.setListElement,
|
||||||
|
setListPath: state.setListPath,
|
||||||
|
setListRank: state.setListRank,
|
||||||
|
}))
|
||||||
|
);
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
|
|
||||||
const [message, setMessage] = useState({
|
const [message, setMessage] = useState({
|
||||||
@@ -129,15 +162,14 @@ export default function CopyImport() {
|
|||||||
if (!selectedKeys.has(index) || (!profile.lightcone?.item_id && Object.keys(profile.relics ?? {}).length == 0)) {
|
if (!selectedKeys.has(index) || (!profile.lightcone?.item_id && Object.keys(profile.relics ?? {}).length == 0)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
const cloned = structuredClone(profile);
|
||||||
...profile,
|
cloned.profile_name = profile.profile_name + ` - Copy: ${avatarCopySelected?.ID}`;
|
||||||
profile_name: profile.profile_name + ` - Copy: ${avatarCopySelected?.ID}`,
|
return cloned as AvatarProfileStore;
|
||||||
} as AvatarProfileStore
|
}).filter((profile): profile is AvatarProfileStore => profile !== null);
|
||||||
}).filter((profile) => profile !== null);
|
|
||||||
|
|
||||||
const newAvatar = {
|
const newAvatar = {
|
||||||
...targetAvatar,
|
...targetAvatar,
|
||||||
profileList: targetAvatar.profileList.concat(newListProfile),
|
profileList: [...targetAvatar.profileList, ...newListProfile],
|
||||||
profileSelect: targetAvatar.profileList.length + newListProfile.length - 1,
|
profileSelect: targetAvatar.profileList.length + newListProfile.length - 1,
|
||||||
}
|
}
|
||||||
setAvatar(newAvatar);
|
setAvatar(newAvatar);
|
||||||
@@ -149,144 +181,169 @@ export default function CopyImport() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg px-2 min-h-[60vh]">
|
<div className="rounded-2xl p-2 sm:p-4 min-h-[50vh] space-y-4">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto space-y-4">
|
||||||
{/* Header */}
|
{/* Header & Filters Card */}
|
||||||
<div className="mb-2">
|
<div className="bg-base-100 rounded-2xl p-4 border border-base-300 shadow-sm space-y-4">
|
||||||
|
{/* Filters Section */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="text-xs font-bold uppercase tracking-wider text-base-content/60">{transI18n("filter")}</div>
|
||||||
|
|
||||||
<div className="mt-2 w-full">
|
<div className="flex flex-wrap items-center gap-3 sm:gap-4">
|
||||||
|
{/* Path Filters */}
|
||||||
<div className="flex items-start flex-col gap-2 mt-2 w-full">
|
<div className="flex flex-wrap items-center gap-1.5 sm:gap-2">
|
||||||
<div>{transI18n("filter")}</div>
|
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => {
|
||||||
<div className="flex flex-wrap justify-start gap-5 md:gap-10 mt-2 w-full">
|
const isActive = !!listPath[key];
|
||||||
{/* Path */}
|
return (
|
||||||
<div>
|
<button
|
||||||
<div className="flex flex-wrap gap-2 justify-start items-center">
|
key={key}
|
||||||
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
|
type="button"
|
||||||
<div
|
onClick={() => setListPath({ ...listPath, [key]: !listPath[key] })}
|
||||||
key={key}
|
className={`h-10 w-10 sm:h-12 sm:w-12 rounded-2xl transition-all duration-150 active-press border flex items-center justify-center cursor-pointer ${
|
||||||
onClick={() => {
|
isActive
|
||||||
setListPath({ ...listPath, [key]: !listPath[key] })
|
? "bg-primary/20 border-primary ring-2 ring-primary/50 shadow-sm scale-105"
|
||||||
}}
|
: "bg-base-200 hover:bg-base-300 border-base-300 opacity-80 hover:opacity-100"
|
||||||
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
|
}`}
|
||||||
style={{
|
title={key}
|
||||||
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
|
>
|
||||||
}}>
|
<Image
|
||||||
<Image
|
unoptimized
|
||||||
unoptimized
|
crossOrigin="anonymous"
|
||||||
crossOrigin="anonymous"
|
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
alt={key}
|
||||||
alt={key}
|
className={`w-6 h-6 sm:w-7 sm:h-7 object-contain ${
|
||||||
className="h-8 w-8 object-contain rounded-md"
|
isActive ? "theme-path-icon-active" : "theme-path-icon"
|
||||||
width={200}
|
}`}
|
||||||
height={200}
|
width={32}
|
||||||
/>
|
height={32}
|
||||||
</div>
|
/>
|
||||||
))}
|
</button>
|
||||||
</div>
|
);
|
||||||
</div>
|
})}
|
||||||
|
|
||||||
{/* Element */}
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-wrap gap-2 justify-start items-center">
|
|
||||||
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
|
|
||||||
<div
|
|
||||||
key={key}
|
|
||||||
onClick={() => {
|
|
||||||
setListElement({ ...listElement, [key]: !listElement[key] })
|
|
||||||
}}
|
|
||||||
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
|
|
||||||
style={{
|
|
||||||
backgroundColor: listElement[key] ? "#374151" : "#6B7280"
|
|
||||||
}}>
|
|
||||||
<Image
|
|
||||||
unoptimized
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
|
||||||
alt={key}
|
|
||||||
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
|
|
||||||
width={200}
|
|
||||||
height={200}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Rank */}
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-wrap gap-2 justify-end items-center">
|
|
||||||
{Object.entries(listRank).map(([key], index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
onClick={() => {
|
|
||||||
setListRank({ ...listRank, [key]: !listRank[key] })
|
|
||||||
}}
|
|
||||||
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
|
|
||||||
style={{
|
|
||||||
backgroundColor: listRank[key] ? "#374151" : "#6B7280"
|
|
||||||
}}>
|
|
||||||
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">{key}*</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
<div className="h-8 w-px bg-base-300 hidden md:block" />
|
||||||
<div className="grid grid-cols-1 gap-2">
|
|
||||||
|
|
||||||
{listAvatar.length > 0 && (
|
{/* Element Filters */}
|
||||||
<div>
|
<div className="flex flex-wrap items-center gap-1.5 sm:gap-2">
|
||||||
<div>{transI18n("characterName")}</div>
|
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => {
|
||||||
<SelectCustomImage
|
const isActive = !!listElement[key];
|
||||||
customSet={listAvatar.map((avatar) => ({
|
return (
|
||||||
value: avatar.ID.toString(),
|
<button
|
||||||
label: getNameChar(locale, transI18n, avatar),
|
key={key}
|
||||||
imageUrl: `${process.env.CDN_URL}/${avatar.Image.AvatarIconPath}`
|
type="button"
|
||||||
}))}
|
onClick={() => setListElement({ ...listElement, [key]: !listElement[key] })}
|
||||||
excludeSet={[]}
|
className={`h-10 w-10 sm:h-12 sm:w-12 rounded-2xl transition-all duration-150 active-press border flex items-center justify-center cursor-pointer ${
|
||||||
selectedCustomSet={avatarCopySelected?.ID.toString() || ""}
|
isActive
|
||||||
placeholder="Character Select"
|
? "bg-primary/20 border-primary ring-2 ring-primary/50 shadow-sm scale-105"
|
||||||
setSelectedCustomSet={(value) => setAvatarCopySelected(mapAvatar[value] || null)}
|
: "bg-base-200 hover:bg-base-300 border-base-300 opacity-80 hover:opacity-100"
|
||||||
/>
|
}`}
|
||||||
</div>
|
title={key}
|
||||||
)}
|
>
|
||||||
|
<Image
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||||
|
alt={key}
|
||||||
|
className="w-6 h-6 sm:w-7 sm:h-7 object-contain drop-shadow-xs"
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-8 w-px bg-base-300 hidden md:block" />
|
||||||
|
|
||||||
|
{/* Rank Filters */}
|
||||||
|
<div className="flex items-center gap-1.5 sm:gap-2">
|
||||||
|
{Object.entries(listRank).map(([key], index) => {
|
||||||
|
const isActive = !!listRank[key];
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setListRank({ ...listRank, [key]: !listRank[key] })}
|
||||||
|
className={`h-10 px-3.5 sm:h-12 sm:px-4.5 rounded-2xl transition-all duration-150 active-press border flex items-center justify-center text-xs sm:text-sm font-mono font-bold cursor-pointer ${
|
||||||
|
isActive
|
||||||
|
? "bg-primary text-primary-content border-primary shadow-sm scale-105"
|
||||||
|
: "bg-base-200 hover:bg-base-300 text-base-content/70 border-base-300"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{key}★
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Character Select Dropdown */}
|
||||||
|
{listAvatar.length > 0 && (
|
||||||
|
<div className="space-y-1.5 pt-2 border-t border-base-300/60">
|
||||||
|
<div className="text-xs font-bold text-base-content/70">{transI18n("characterName")}</div>
|
||||||
|
<SelectCustomImage
|
||||||
|
customSet={listAvatar.map((avatar) => ({
|
||||||
|
value: avatar.ID.toString(),
|
||||||
|
label: getNameChar(locale, transI18n, avatar),
|
||||||
|
imageUrl: `${process.env.CDN_URL}/${avatar.Image.AvatarIconPath}`
|
||||||
|
}))}
|
||||||
|
excludeSet={[]}
|
||||||
|
selectedCustomSet={avatarCopySelected?.ID.toString() || ""}
|
||||||
|
placeholder="Character Select"
|
||||||
|
setSelectedCustomSet={(value) => setAvatarCopySelected(mapAvatar[value] || null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Selection Action Bar */}
|
||||||
{/* Selection Info */}
|
<div className="bg-base-200 border border-base-300 rounded-2xl p-3.5 flex flex-wrap items-center justify-between gap-3 shadow-xs">
|
||||||
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-blue-400 font-medium">
|
<span className="text-xs sm:text-sm font-bold text-base-content">
|
||||||
{transI18n("selectedProfiles")}: {selectedProfiles.length}
|
{transI18n("selectedProfiles")}:
|
||||||
</span>
|
</span>
|
||||||
|
<span className="badge badge-primary font-mono font-bold text-xs">
|
||||||
|
{selectedProfiles.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={clearSelection}
|
onClick={clearSelection}
|
||||||
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
|
className="btn btn-xs sm:btn-sm btn-ghost text-error hover:bg-error/10 border border-error/30 rounded-xl"
|
||||||
>
|
>
|
||||||
{transI18n("clearAll")}
|
{transI18n("clearAll")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={selectAll}
|
onClick={selectAll}
|
||||||
className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
|
className="btn btn-xs sm:btn-sm btn-ghost text-primary hover:bg-primary/10 border border-primary/30 rounded-xl"
|
||||||
|
>
|
||||||
{transI18n("selectAll")}
|
{transI18n("selectAll")}
|
||||||
</button>
|
</button>
|
||||||
{selectedProfiles.length > 0 && (
|
{selectedProfiles.length > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={handleCopy}
|
onClick={handleCopy}
|
||||||
className="text-success/75 hover:text-success text-sm font-medium px-3 py-1 border border-success/75 rounded hover:bg-success/10 transition-colors cursor-pointer">
|
className="btn btn-xs sm:btn-sm btn-primary rounded-xl font-bold shadow-xs"
|
||||||
|
>
|
||||||
{transI18n("copy")}
|
{transI18n("copy")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{message.type && message.text && (
|
|
||||||
<div className={(message.type == "error" ? "text-error" : "text-success") + " text-lg mt-2"} >{message.type == "error" ? "😭" : "🎉"} {message.text}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{message.type && message.text && (
|
||||||
|
<div className={`p-3 rounded-xl border text-xs sm:text-sm font-semibold flex items-center gap-2 ${
|
||||||
|
message.type === "error"
|
||||||
|
? "bg-error/10 text-error border-error/30"
|
||||||
|
: "bg-success/10 text-success border-success/30"
|
||||||
|
}`}>
|
||||||
|
<span>{message.type === "error" ? "⚠️" : "🎉"}</span>
|
||||||
|
<span>{message.text}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Character Grid */}
|
{/* Character Grid */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
{avatarCopySelected && avatars[avatarCopySelected?.ID.toString()]?.profileList.map((profile, index) => {
|
{avatarCopySelected && avatars[avatarCopySelected?.ID.toString()]?.profileList.map((profile, index) => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { useTranslations } from "next-intl";
|
|||||||
import { avatarEnkaDetailSchema, enkaResponseSchema } from "@/zod";
|
import { avatarEnkaDetailSchema, enkaResponseSchema } from "@/zod";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import useDetailDataStore from "@/stores/detailDataStore";
|
import useDetailDataStore from "@/stores/detailDataStore";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
|
||||||
type AvatarEnkaDetailInput = z.infer<typeof avatarEnkaDetailSchema>;
|
type AvatarEnkaDetailInput = z.infer<typeof avatarEnkaDetailSchema>;
|
||||||
|
|
||||||
@@ -40,11 +41,33 @@ export default function EnkaImport() {
|
|||||||
selectedCharacters,
|
selectedCharacters,
|
||||||
setSelectedCharacters,
|
setSelectedCharacters,
|
||||||
setEnkaData,
|
setEnkaData,
|
||||||
} = useEnkaStore();
|
} = useEnkaStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
uidInput: state.uidInput,
|
||||||
|
setUidInput: state.setUidInput,
|
||||||
|
enkaData: state.enkaData,
|
||||||
|
selectedCharacters: state.selectedCharacters,
|
||||||
|
setSelectedCharacters: state.setSelectedCharacters,
|
||||||
|
setEnkaData: state.setEnkaData,
|
||||||
|
}))
|
||||||
|
);
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
const { avatars, setAvatar } = useUserDataStore();
|
const { avatars, setAvatar } = useUserDataStore(
|
||||||
const { setIsOpenImport } = useModelStore()
|
useShallow((state) => ({
|
||||||
const { mapAvatar } = useDetailDataStore()
|
avatars: state.avatars,
|
||||||
|
setAvatar: state.setAvatar,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const { setIsOpenImport } = useModelStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
setIsOpenImport: state.setIsOpenImport,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { mapAvatar } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
mapAvatar: state.mapAvatar,
|
||||||
|
}))
|
||||||
|
)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [Error, setError] = useState("")
|
const [Error, setError] = useState("")
|
||||||
const validCharacters = enkaData?.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId]) ?? []
|
const validCharacters = enkaData?.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId]) ?? []
|
||||||
@@ -115,81 +138,95 @@ export default function EnkaImport() {
|
|||||||
|
|
||||||
const filterData = parsed.data.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId] && selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatarId))
|
const filterData = parsed.data.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId] && selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatarId))
|
||||||
filterData.forEach((character) => {
|
filterData.forEach((character) => {
|
||||||
const newAvatar = { ...listAvatars[character.avatarId.toString()] }
|
const currentAvatar = listAvatars[character.avatarId.toString()]
|
||||||
if (Object.keys(newAvatar).length !== 0) {
|
if (currentAvatar && Object.keys(currentAvatar).length !== 0) {
|
||||||
newAvatar.level = character.level ?? 0
|
const newProfileList = [...(currentAvatar.profileList || [])]
|
||||||
newAvatar.promotion = character.promotion ?? 0
|
const newProfile = converterOneEnkaDataToAvatarStore(character, newProfileList.length)
|
||||||
newAvatar.data = {
|
|
||||||
rank: character.rank ?? 0,
|
|
||||||
skills: character.skillTreeList.reduce((acc, skill) => {
|
|
||||||
acc[skill.pointId] = skill.level;
|
|
||||||
return acc;
|
|
||||||
}, {} as Record<string, number>),
|
|
||||||
}
|
|
||||||
const newProfile = converterOneEnkaDataToAvatarStore(character, newAvatar.profileList.length)
|
|
||||||
if (newProfile) {
|
if (newProfile) {
|
||||||
newAvatar.profileList.push(newProfile)
|
newProfileList.push(newProfile)
|
||||||
newAvatar.profileSelect = newAvatar.profileList.length - 1
|
}
|
||||||
|
const newAvatar = {
|
||||||
|
...currentAvatar,
|
||||||
|
level: character.level ?? 0,
|
||||||
|
promotion: character.promotion ?? 0,
|
||||||
|
data: {
|
||||||
|
rank: character.rank ?? 0,
|
||||||
|
skills: character.skillTreeList.reduce((acc, skill) => {
|
||||||
|
acc[skill.pointId] = skill.level;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number>),
|
||||||
|
},
|
||||||
|
profileList: newProfileList,
|
||||||
|
profileSelect: newProfile ? newProfileList.length - 1 : currentAvatar.profileSelect,
|
||||||
}
|
}
|
||||||
setAvatar(newAvatar)
|
setAvatar(newAvatar)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
setIsOpenImport(false)
|
setIsOpenImport(false)
|
||||||
toast.success(transI18n("importEnkaDataSuccess"))
|
toast.success(transI18n("importEnkaDataSuccess"))
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-slate-900 p-6">
|
<div className="bg-base-100 p-4 sm:p-6 rounded-2xl">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="space-y-4">
|
||||||
<h1 className="text-white text-2xl font-bold mb-6">HSR UID</h1>
|
<h1 className="text-base-content text-xl sm:text-2xl font-bold">HSR UID</h1>
|
||||||
|
|
||||||
<div className="flex gap-4 items-center mb-6">
|
<div className="flex flex-wrap gap-3 items-center">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={uidInput}
|
value={uidInput}
|
||||||
onChange={(e) => setUidInput(e.target.value)}
|
onChange={(e) => setUidInput(e.target.value)}
|
||||||
className="bg-slate-800 text-white px-4 py-3 rounded-lg border border-slate-700 flex-1 max-w-md focus:outline-none focus:border-blue-500"
|
className="input input-bordered w-full max-w-xs rounded-xl bg-base-200 border-base-300 text-sm focus:border-primary"
|
||||||
placeholder="Enter UID"
|
placeholder="Enter UID"
|
||||||
/>
|
/>
|
||||||
<button onClick={handlerFetchData} className="btn btn-success rounded-lg transition-colors">
|
<button onClick={handlerFetchData} className="btn btn-sm btn-success rounded-xl font-bold">
|
||||||
{transI18n("getData")}
|
{transI18n("getData")}
|
||||||
</button>
|
</button>
|
||||||
{selectedCharacters.length > 0 && (
|
{selectedCharacters.length > 0 && (
|
||||||
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
|
<button onClick={handleImport} className="btn btn-sm btn-primary rounded-xl font-bold shadow-xs">
|
||||||
{transI18n("import")}
|
{transI18n("import")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{Error && (
|
{Error && (
|
||||||
<div className="text-error text-base mt-2">😭 {Error}</div>
|
<div className="text-error text-sm font-semibold mt-2">😭 {Error}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Player Info */}
|
{/* Player Info */}
|
||||||
{enkaData && (
|
{enkaData && (
|
||||||
<div className="text-white space-y-2">
|
<div className="bg-base-200 p-4 rounded-xl border border-base-300 text-base-content space-y-1">
|
||||||
<div className="text-lg">Name: {enkaData.detailInfo.nickname}</div>
|
<div className="text-sm font-semibold">Name: <span className="font-bold text-primary">{enkaData.detailInfo.nickname}</span></div>
|
||||||
<div className="text-lg">UID: {enkaData.detailInfo.uid}</div>
|
<div className="text-sm font-semibold">UID: <span className="font-mono">{enkaData.detailInfo.uid}</span></div>
|
||||||
<div className="text-lg">Level: {enkaData.detailInfo.level}</div>
|
<div className="text-sm font-semibold">Level: <span className="font-bold">{enkaData.detailInfo.level}</span></div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Selection Info */}
|
{/* Selection Info */}
|
||||||
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
|
<div className="flex flex-wrap items-center justify-between gap-3 bg-base-200 border border-base-300 p-3.5 rounded-2xl shadow-xs">
|
||||||
<span className="text-blue-400 font-medium">
|
<div className="flex items-center gap-2">
|
||||||
{transI18n("selectedCharacters")}: {selectedCharacters.length}
|
<span className="text-xs sm:text-sm font-bold text-base-content">
|
||||||
</span>
|
{transI18n("selectedCharacters")}:
|
||||||
<button
|
</span>
|
||||||
onClick={clearSelection}
|
<span className="badge badge-primary font-mono font-bold text-xs">
|
||||||
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
|
{selectedCharacters.length}
|
||||||
>
|
</span>
|
||||||
{transI18n("clearAll")}
|
</div>
|
||||||
</button>
|
<div className="flex items-center gap-2">
|
||||||
<button onClick={selectAll} className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
|
<button
|
||||||
{transI18n("selectAll")}
|
onClick={clearSelection}
|
||||||
</button>
|
className="btn btn-xs sm:btn-sm btn-ghost text-error hover:bg-error/10 border border-error/30 rounded-xl"
|
||||||
|
>
|
||||||
|
{transI18n("clearAll")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={selectAll}
|
||||||
|
className="btn btn-xs sm:btn-sm btn-ghost text-primary hover:bg-primary/10 border border-primary/30 rounded-xl"
|
||||||
|
>
|
||||||
|
{transI18n("selectAll")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { toast } from "react-toastify";
|
|||||||
import { converterOneFreeSRDataToAvatarStore } from "@/helper";
|
import { converterOneFreeSRDataToAvatarStore } from "@/helper";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import useDetailDataStore from "@/stores/detailDataStore";
|
import useDetailDataStore from "@/stores/detailDataStore";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
type FreeSRJsonInput = z.infer<typeof freeSrJsonSchema>;
|
type FreeSRJsonInput = z.infer<typeof freeSrJsonSchema>;
|
||||||
@@ -38,12 +39,32 @@ const toCharacterInfoCard = (data: FreeSRJsonInput, character: FreeSRJsonInput["
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function FreeSRImport() {
|
export default function FreeSRImport() {
|
||||||
const { avatars, setAvatar } = useUserDataStore();
|
const { avatars, setAvatar } = useUserDataStore(
|
||||||
const { mapAvatar } = useDetailDataStore();
|
useShallow((state) => ({
|
||||||
const { setIsOpenImport } = useModelStore()
|
avatars: state.avatars,
|
||||||
|
setAvatar: state.setAvatar,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const { mapAvatar } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
mapAvatar: state.mapAvatar,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
const { setIsOpenImport } = useModelStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
setIsOpenImport: state.setIsOpenImport,
|
||||||
|
}))
|
||||||
|
)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [Error, setError] = useState("")
|
const [Error, setError] = useState("")
|
||||||
const { freeSRData, setFreeSRData, selectedCharacters, setSelectedCharacters } = useFreeSRStore()
|
const { freeSRData, setFreeSRData, selectedCharacters, setSelectedCharacters } = useFreeSRStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
freeSRData: state.freeSRData,
|
||||||
|
setFreeSRData: state.setFreeSRData,
|
||||||
|
selectedCharacters: state.selectedCharacters,
|
||||||
|
setSelectedCharacters: state.setSelectedCharacters,
|
||||||
|
}))
|
||||||
|
)
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
const parsedFreeSRData = freeSrJsonSchema.safeParse(freeSRData)
|
const parsedFreeSRData = freeSrJsonSchema.safeParse(freeSRData)
|
||||||
const validFreeSRData = parsedFreeSRData.success ? parsedFreeSRData.data : null
|
const validFreeSRData = parsedFreeSRData.success ? parsedFreeSRData.data : null
|
||||||
@@ -142,66 +163,79 @@ export default function FreeSRImport() {
|
|||||||
const listAvatars = { ...avatars }
|
const listAvatars = { ...avatars }
|
||||||
const filterData = Object.values(parsed.data.avatars).filter((character) => selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id))
|
const filterData = Object.values(parsed.data.avatars).filter((character) => selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id))
|
||||||
filterData.forEach((character) => {
|
filterData.forEach((character) => {
|
||||||
const newAvatar = { ...listAvatars[character.avatar_id] }
|
const currentAvatar = listAvatars[character.avatar_id]
|
||||||
if (Object.keys(newAvatar).length !== 0) {
|
if (currentAvatar && Object.keys(currentAvatar).length !== 0) {
|
||||||
newAvatar.level = character.level
|
const newProfileList = [...(currentAvatar.profileList || [])]
|
||||||
newAvatar.promotion = character.promotion
|
const newProfile = converterOneFreeSRDataToAvatarStore(parsed.data, newProfileList.length, character.avatar_id)
|
||||||
newAvatar.data = {
|
|
||||||
rank: character.data.rank ?? 0,
|
|
||||||
skills: character.data.skills
|
|
||||||
}
|
|
||||||
const newProfile = converterOneFreeSRDataToAvatarStore(parsed.data, newAvatar.profileList.length, character.avatar_id)
|
|
||||||
if (newProfile) {
|
if (newProfile) {
|
||||||
newAvatar.profileList.push(newProfile)
|
newProfileList.push(newProfile)
|
||||||
newAvatar.profileSelect = newAvatar.profileList.length - 1
|
}
|
||||||
|
const newAvatar = {
|
||||||
|
...currentAvatar,
|
||||||
|
level: character.level,
|
||||||
|
promotion: character.promotion,
|
||||||
|
data: {
|
||||||
|
rank: character.data.rank ?? 0,
|
||||||
|
skills: character.data.skills
|
||||||
|
},
|
||||||
|
profileList: newProfileList,
|
||||||
|
profileSelect: newProfile ? newProfileList.length - 1 : currentAvatar.profileSelect,
|
||||||
}
|
}
|
||||||
setAvatar(newAvatar)
|
setAvatar(newAvatar)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
setIsOpenImport(false)
|
setIsOpenImport(false)
|
||||||
toast.success(transI18n("importFreeSRDataSuccess"))
|
toast.success(transI18n("importFreeSRDataSuccess"))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-slate-900 p-6">
|
<div className="bg-base-100 p-4 sm:p-6 rounded-2xl">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-8">
|
<div className="space-y-4">
|
||||||
<h1 className="text-white text-2xl font-bold mb-6">{transI18n("freeSRImport")}</h1>
|
<h1 className="text-base-content text-xl sm:text-2xl font-bold">{transI18n("freeSRImport")}</h1>
|
||||||
|
|
||||||
<div className="flex gap-4 items-center mb-6">
|
<div className="flex flex-wrap gap-4 items-center">
|
||||||
<fieldset className="fieldset">
|
<fieldset className="fieldset">
|
||||||
<legend className="fieldset-legend">{transI18n("pickAFile")}</legend>
|
<legend className="fieldset-legend text-xs font-semibold">{transI18n("pickAFile")}</legend>
|
||||||
<input type="file" accept=".json" className="file-input file-input-info" onChange={handlerReadFile} />
|
<input type="file" accept=".json" className="file-input file-input-sm file-input-bordered rounded-xl" onChange={handlerReadFile} />
|
||||||
<label className="label">{transI18n("onlySupportFreeSRJsonFile")}</label>
|
<label className="label text-[11px] text-base-content/60">{transI18n("onlySupportFreeSRJsonFile")}</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
{selectedCharacters.length > 0 && (
|
{selectedCharacters.length > 0 && (
|
||||||
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
|
<button onClick={handleImport} className="btn btn-sm btn-primary rounded-xl font-bold shadow-xs">
|
||||||
{transI18n("import")}
|
{transI18n("import")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{Error && (
|
{Error && (
|
||||||
<div className="text-error text-base mt-2">😭 {Error}</div>
|
<div className="text-error text-sm font-semibold mt-2">😭 {Error}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
||||||
{/* Selection Info */}
|
{/* Selection Info */}
|
||||||
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
|
<div className="flex flex-wrap items-center justify-between gap-3 bg-base-200 border border-base-300 p-3.5 rounded-2xl shadow-xs">
|
||||||
<span className="text-blue-400 font-medium">
|
<div className="flex items-center gap-2">
|
||||||
{transI18n("selectedCharacters")}: {selectedCharacters.length}
|
<span className="text-xs sm:text-sm font-bold text-base-content">
|
||||||
</span>
|
{transI18n("selectedCharacters")}:
|
||||||
<button
|
</span>
|
||||||
onClick={clearSelection}
|
<span className="badge badge-primary font-mono font-bold text-xs">
|
||||||
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
|
{selectedCharacters.length}
|
||||||
>
|
</span>
|
||||||
{transI18n("clearAll")}
|
</div>
|
||||||
</button>
|
<div className="flex items-center gap-2">
|
||||||
<button onClick={selectAll} className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
|
<button
|
||||||
{transI18n("selectAll")}
|
onClick={clearSelection}
|
||||||
</button>
|
className="btn btn-xs sm:btn-sm btn-ghost text-error hover:bg-error/10 border border-error/30 rounded-xl"
|
||||||
|
>
|
||||||
|
{transI18n("clearAll")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={selectAll}
|
||||||
|
className="btn btn-xs sm:btn-sm btn-ghost text-primary hover:bg-primary/10 border border-primary/30 rounded-xl"
|
||||||
|
>
|
||||||
|
{transI18n("selectAll")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ import { useTranslations } from "next-intl";
|
|||||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||||
import useDetailDataStore from "@/stores/detailDataStore";
|
import useDetailDataStore from "@/stores/detailDataStore";
|
||||||
import { calcRarity, getLocaleName } from "@/helper";
|
import { calcRarity, getLocaleName } from "@/helper";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
import { Search, X } from "lucide-react";
|
||||||
|
|
||||||
export default function LightconeBar() {
|
export default function LightconeBar() {
|
||||||
const { locale } = useLocaleStore()
|
const { locale } = useLocaleStore(
|
||||||
|
useShallow((state) => ({ locale: state.locale }))
|
||||||
|
)
|
||||||
const {
|
const {
|
||||||
avatarSelected,
|
avatarSelected,
|
||||||
mapLightconePathActive,
|
mapLightconePathActive,
|
||||||
@@ -21,10 +25,34 @@ export default function LightconeBar() {
|
|||||||
setMapLightconeRankActive,
|
setMapLightconeRankActive,
|
||||||
lightconeSearch,
|
lightconeSearch,
|
||||||
setLightconeSearch
|
setLightconeSearch
|
||||||
} = useCurrentDataStore()
|
} = useCurrentDataStore(
|
||||||
const { setAvatar, avatars } = useUserDataStore()
|
useShallow((state) => ({
|
||||||
const { setIsOpenLightcone } = useModelStore()
|
avatarSelected: state.avatarSelected,
|
||||||
const { mapLightCone, baseType } = useDetailDataStore()
|
mapLightconePathActive: state.mapLightconePathActive,
|
||||||
|
mapLightconeRankActive: state.mapLightconeRankActive,
|
||||||
|
setMapLightconePathActive: state.setMapLightconePathActive,
|
||||||
|
setMapLightconeRankActive: state.setMapLightconeRankActive,
|
||||||
|
lightconeSearch: state.lightconeSearch,
|
||||||
|
setLightconeSearch: state.setLightconeSearch,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { setAvatar, avatars } = useUserDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
setAvatar: state.setAvatar,
|
||||||
|
avatars: state.avatars,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { setIsOpenLightcone } = useModelStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
setIsOpenLightcone: state.setIsOpenLightcone,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { mapLightCone, baseType } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
mapLightCone: state.mapLightCone,
|
||||||
|
baseType: state.baseType,
|
||||||
|
}))
|
||||||
|
)
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
|
|
||||||
const listLightcone = useMemo(() => {
|
const listLightcone = useMemo(() => {
|
||||||
@@ -39,10 +67,13 @@ export default function LightconeBar() {
|
|||||||
const allRankFalse = !Object.values(mapLightconeRankActive).some(v => v)
|
const allRankFalse = !Object.values(mapLightconeRankActive).some(v => v)
|
||||||
const allPathFalse = !Object.values(mapLightconePathActive).some(v => v)
|
const allPathFalse = !Object.values(mapLightconePathActive).some(v => v)
|
||||||
|
|
||||||
list = list.filter(item =>
|
list = list.filter(item => {
|
||||||
(allRankFalse || mapLightconeRankActive[item.Rarity]) &&
|
const itemRank = calcRarity(item.Rarity).toString();
|
||||||
(allPathFalse || mapLightconePathActive[item.BaseType])
|
return (
|
||||||
)
|
(allRankFalse || !!mapLightconeRankActive[itemRank] || !!mapLightconeRankActive[item.Rarity]) &&
|
||||||
|
(allPathFalse || !!mapLightconePathActive[item.BaseType])
|
||||||
|
);
|
||||||
|
})
|
||||||
|
|
||||||
list.sort((a, b) => {
|
list.sort((a, b) => {
|
||||||
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
|
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
|
||||||
@@ -53,89 +84,125 @@ export default function LightconeBar() {
|
|||||||
return list
|
return list
|
||||||
}, [mapLightCone, mapLightconePathActive, mapLightconeRankActive, lightconeSearch, locale])
|
}, [mapLightCone, mapLightconePathActive, mapLightconeRankActive, lightconeSearch, locale])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
{/* Search & Filters */}
|
||||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
<div className="p-3 rounded-2xl bg-base-200 border border-base-300 space-y-2.5">
|
||||||
{transI18n("lightConeSetting")}
|
{/* Search Bar */}
|
||||||
</h3>
|
<label className="input input-sm input-bordered flex items-center gap-2 bg-base-100 border-base-300 rounded-lg w-full h-8 px-2.5 shadow-xs">
|
||||||
</div>
|
<Search className="w-4 h-4 text-base-content/60 shrink-0" />
|
||||||
<div className="mt-2 w-full">
|
|
||||||
<div className="flex items-start flex-col gap-2">
|
|
||||||
<div>Search</div>
|
|
||||||
<input
|
<input
|
||||||
value={lightconeSearch}
|
value={lightconeSearch}
|
||||||
onChange={(e) => setLightconeSearch(e.target.value)}
|
onChange={(e) => setLightconeSearch(e.target.value)}
|
||||||
type="text" placeholder="LightCone Name" className="input input-accent mt-1 w-full"
|
type="text"
|
||||||
|
placeholder={`${transI18n("lightcones")}...`}
|
||||||
|
className="grow text-xs bg-transparent border-none outline-none focus:outline-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
{lightconeSearch && (
|
||||||
<div className="flex items-start flex-col gap-2 mt-2 w-full">
|
<button
|
||||||
<div>Filter</div>
|
type="button"
|
||||||
<div className="flex flex-row flex-wrap justify-between mt-1 w-full">
|
onClick={() => setLightconeSearch("")}
|
||||||
<div className="flex flex-wrap mb-1 mx-1 gap-2">
|
className="text-base-content/40 hover:text-base-content"
|
||||||
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
|
>
|
||||||
<div
|
<X className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{/* Filter Chips */}
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
{/* Path Filter Chips */}
|
||||||
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
|
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => {
|
||||||
|
const isActive = !!mapLightconePathActive[key];
|
||||||
|
return (
|
||||||
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMapLightconePathActive({ ...mapLightconePathActive, [key]: !mapLightconePathActive[key] })
|
setMapLightconePathActive({ ...mapLightconePathActive, [key]: !mapLightconePathActive[key] })
|
||||||
}}
|
}}
|
||||||
className="h-9.5 w-9.5 md:h-12.5 md:w-12.5 hover:bg-gray-600 grid place-items-center rounded-md shadow-lg cursor-pointer"
|
className={`h-7 w-7 rounded-lg transition-all duration-150 active-press border flex items-center justify-center ${
|
||||||
style={{
|
isActive
|
||||||
backgroundColor: mapLightconePathActive[key] ? "#374151" : "#6B7280"
|
? "bg-primary/20 border-primary ring-1.5 ring-primary/50 shadow-xs scale-105"
|
||||||
}}
|
: "bg-base-200 hover:bg-base-300 border-base-content/10 opacity-80 hover:opacity-100"
|
||||||
|
}`}
|
||||||
|
title={key}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
unoptimized
|
unoptimized
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||||
alt={key}
|
alt={key}
|
||||||
className="h-7 w-7 md:h-8 md:w-8 object-contain rounded-md"
|
className={`w-4.5 h-4.5 object-contain ${
|
||||||
width={200}
|
isActive ? "theme-path-icon-active" : "theme-path-icon"
|
||||||
height={200}
|
}`}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
/>
|
/>
|
||||||
</div>
|
</button>
|
||||||
))}
|
);
|
||||||
</div>
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap mb-1 mx-1 gap-2">
|
{/* Rarity Filter Chips */}
|
||||||
{Object.keys(mapLightconeRankActive).map((key, index) => (
|
<div className="flex items-center gap-1.5">
|
||||||
<div
|
{Object.keys(mapLightconeRankActive).map((key) => {
|
||||||
key={index}
|
const isActive = !!mapLightconeRankActive[key];
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMapLightconeRankActive({ ...mapLightconeRankActive, [key]: !mapLightconeRankActive[key] })
|
setMapLightconeRankActive({ ...mapLightconeRankActive, [key]: !mapLightconeRankActive[key] })
|
||||||
}}
|
}}
|
||||||
className="h-9.5 w-9.5 md:h-12.5 md:w-12.5 hover:bg-gray-600 grid place-items-center rounded-md shadow-lg cursor-pointer"
|
className={`px-3 py-1 rounded-xl text-xs font-mono font-bold transition-all duration-200 active-press border ${
|
||||||
style={{
|
isActive
|
||||||
backgroundColor: mapLightconeRankActive[key] ? "#374151" : "#6B7280"
|
? "bg-primary text-primary-content border-primary shadow-xs scale-105"
|
||||||
}}
|
: "bg-base-100 border-base-300 text-base-content/70 hover:text-base-content hover:bg-base-200"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">
|
{key}★
|
||||||
{key}*
|
</button>
|
||||||
</div>
|
);
|
||||||
</div>
|
})}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-8 mt-2 gap-2">
|
|
||||||
{listLightcone.map((item, index) => (
|
{/* Results Grid */}
|
||||||
<div key={index} onClick={() => {
|
<div className="grid grid-cols-2 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-7 gap-3 auto-rows-fr max-h-[58vh] overflow-y-auto pr-1">
|
||||||
if (avatarSelected) {
|
{listLightcone.map((item) => (
|
||||||
const avatar = avatars[avatarSelected?.ID?.toString()]
|
<div
|
||||||
avatar.profileList[avatar.profileSelect].lightcone = {
|
key={item.ID}
|
||||||
level: 80,
|
className="h-full flex flex-col"
|
||||||
item_id: item.ID,
|
onClick={() => {
|
||||||
rank: 1,
|
if (avatarSelected) {
|
||||||
promotion: 6
|
const avatar = avatars[avatarSelected.ID.toString()]
|
||||||
|
if (avatar) {
|
||||||
|
const profileIndex = avatar.profileSelect
|
||||||
|
const currentProfile = avatar.profileList?.[profileIndex]
|
||||||
|
if (currentProfile) {
|
||||||
|
const newProfileList = [...avatar.profileList]
|
||||||
|
newProfileList[profileIndex] = {
|
||||||
|
...currentProfile,
|
||||||
|
lightcone: {
|
||||||
|
level: 80,
|
||||||
|
item_id: item.ID,
|
||||||
|
rank: 1,
|
||||||
|
promotion: 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setAvatar({
|
||||||
|
...avatar,
|
||||||
|
profileList: newProfileList
|
||||||
|
})
|
||||||
|
setIsOpenLightcone(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setAvatar({ ...avatar })
|
}}
|
||||||
setIsOpenLightcone(false)
|
>
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<LightconeCard data={item} />
|
<LightconeCard data={item} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -8,14 +8,30 @@ import Image from "next/image";
|
|||||||
import { ASEvent, MonsterStore } from "@/types";
|
import { ASEvent, MonsterStore } from "@/types";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import useDetailDataStore from "@/stores/detailDataStore";
|
import useDetailDataStore from "@/stores/detailDataStore";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
|
||||||
export default function AsBar() {
|
export default function AsBar() {
|
||||||
const { locale } = useLocaleStore()
|
const { locale } = useLocaleStore(
|
||||||
|
useShallow((state) => ({ locale: state.locale }))
|
||||||
|
)
|
||||||
const {
|
const {
|
||||||
as_config,
|
as_config,
|
||||||
setAsConfig
|
setAsConfig
|
||||||
} = useUserDataStore()
|
} = useUserDataStore(
|
||||||
const { mapMonster, mapAS, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
|
useShallow((state) => ({
|
||||||
|
as_config: state.as_config,
|
||||||
|
setAsConfig: state.setAsConfig,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { mapMonster, mapAS, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
mapMonster: state.mapMonster,
|
||||||
|
mapAS: state.mapAS,
|
||||||
|
damageType: state.damageType,
|
||||||
|
hardLevelConfig: state.hardLevelConfig,
|
||||||
|
eliteConfig: state.eliteConfig,
|
||||||
|
}))
|
||||||
|
)
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
|
|
||||||
const challengeSelected = useMemo(() => {
|
const challengeSelected = useMemo(() => {
|
||||||
@@ -108,6 +124,13 @@ export default function AsBar() {
|
|||||||
|
|
||||||
if (targetEventList.length > 0) {
|
if (targetEventList.length > 0) {
|
||||||
newBattleConfig.stage_id = targetEventList[0].ID
|
newBattleConfig.stage_id = targetEventList[0].ID
|
||||||
|
if (targetEventList[0].Invasion) {
|
||||||
|
newBattleConfig.blessings.push({
|
||||||
|
id: targetEventList[0].Invasion.MazeBuff.ID,
|
||||||
|
level: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for (const wave of targetEventList[0].MonsterList) {
|
for (const wave of targetEventList[0].MonsterList) {
|
||||||
const newWave: MonsterStore[] = []
|
const newWave: MonsterStore[] = []
|
||||||
for (const value of Object.values(wave)) {
|
for (const value of Object.values(wave)) {
|
||||||
@@ -135,10 +158,10 @@ export default function AsBar() {
|
|||||||
|
|
||||||
if (!mapAS) return null
|
if (!mapAS) return null
|
||||||
return (
|
return (
|
||||||
<div className="py-8 relative">
|
<div className="pt-4 pb-2 relative space-y-4">
|
||||||
|
|
||||||
{/* Title Card */}
|
{/* Title Card */}
|
||||||
<div className="rounded-xl p-4 mb-2 border border-warning">
|
<div className="rounded-xl p-4 border border-warning">
|
||||||
<div className="mb-4 w-full">
|
<div className="mb-4 w-full">
|
||||||
<SelectCustomText
|
<SelectCustomText
|
||||||
customSet={Object.values(mapAS).sort((a, b) => b.ID - a.ID).map((as) => ({
|
customSet={Object.values(mapAS).sort((a, b) => b.ID - a.ID).map((as) => ({
|
||||||
@@ -209,13 +232,13 @@ export default function AsBar() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Turbulence Buff */}
|
{/* Turbulence Buff */}
|
||||||
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
|
<div className="pt-3 border-t border-base-content/10 space-y-2">
|
||||||
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2>
|
<h2 className="text-xl font-bold text-info">{transI18n("turbulenceBuff")}</h2>
|
||||||
{challengeSelected ? (
|
{challengeSelected ? (
|
||||||
challengeSelected.MazeBuff.map((buff, i) => (
|
challengeSelected.MazeBuff.map((buff, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-base"
|
className="text-sm text-base-content/85 leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: replaceByParam(
|
__html: replaceByParam(
|
||||||
getLocaleName(locale, buff?.Desc) || "",
|
getLocaleName(locale, buff?.Desc) || "",
|
||||||
@@ -225,7 +248,7 @@ export default function AsBar() {
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
|
<div className="text-sm text-base-content/50 italic">{transI18n("noTurbulenceBuff")}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -246,7 +269,7 @@ export default function AsBar() {
|
|||||||
const targetEvent = eventList[0];
|
const targetEvent = eventList[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
|
<div key={i} className="rounded-xl p-4 border border-warning">
|
||||||
<h2 className="text-2xl font-bold mb-6 text-info">{side.wave}</h2>
|
<h2 className="text-2xl font-bold mb-6 text-info">{side.wave}</h2>
|
||||||
|
|
||||||
{targetEvent?.MonsterList?.map((wave, waveIndex) => (
|
{targetEvent?.MonsterList?.map((wave, waveIndex) => (
|
||||||
|
|||||||
@@ -17,19 +17,37 @@ import { useTranslations } from "next-intl";
|
|||||||
import useGlobalStore from "@/stores/globalStore";
|
import useGlobalStore from "@/stores/globalStore";
|
||||||
import useDetailDataStore from "@/stores/detailDataStore";
|
import useDetailDataStore from "@/stores/detailDataStore";
|
||||||
import { MonsterDetail } from "@/types";
|
import { MonsterDetail } from "@/types";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
|
||||||
export default function CeBar() {
|
export default function CeBar() {
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [showSearchWaveId, setShowSearchWaveId] = useState<number | null>(null);
|
const [showSearchWaveId, setShowSearchWaveId] = useState<number | null>(null);
|
||||||
const { ce_config, setCeConfig } = useUserDataStore()
|
const { ce_config, setCeConfig } = useUserDataStore(
|
||||||
const { mapMonster, stage, damageType } = useDetailDataStore()
|
useShallow((state) => ({
|
||||||
const { locale } = useLocaleStore()
|
ce_config: state.ce_config,
|
||||||
|
setCeConfig: state.setCeConfig,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { mapMonster, stage, damageType } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
mapMonster: state.mapMonster,
|
||||||
|
stage: state.stage,
|
||||||
|
damageType: state.damageType,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { locale } = useLocaleStore(
|
||||||
|
useShallow((state) => ({ locale: state.locale }))
|
||||||
|
)
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
const [showSearchStage, setShowSearchStage] = useState(false)
|
const [showSearchStage, setShowSearchStage] = useState(false)
|
||||||
const [stageSearchTerm, setStageSearchTerm] = useState("")
|
const [stageSearchTerm, setStageSearchTerm] = useState("")
|
||||||
const [stagePage, setStagePage] = useState(1)
|
const [stagePage, setStagePage] = useState(1)
|
||||||
const { extraData, setExtraData } = useGlobalStore()
|
const { extraData, setExtraData } = useGlobalStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
extraData: state.extraData,
|
||||||
|
setExtraData: state.setExtraData,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
const pageSize = 30
|
const pageSize = 30
|
||||||
|
|
||||||
@@ -59,10 +77,6 @@ export default function CeBar() {
|
|||||||
[monsterPage, filteredMonsters]
|
[monsterPage, filteredMonsters]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMonsterPage(1)
|
|
||||||
}, [searchTerm])
|
|
||||||
|
|
||||||
const stageList = useMemo(() => Object.values(stage).map((item) => ({
|
const stageList = useMemo(() => Object.values(stage).map((item) => ({
|
||||||
id: item.ID.toString(),
|
id: item.ID.toString(),
|
||||||
name: `${getLocaleName(locale, item.Name)} (${item.ID})`,
|
name: `${getLocaleName(locale, item.Name)} (${item.ID})`,
|
||||||
@@ -79,11 +93,6 @@ export default function CeBar() {
|
|||||||
|
|
||||||
const hasMorePages = useMemo(() => stagePage * pageSize < filteredStages.length, [stagePage, filteredStages])
|
const hasMorePages = useMemo(() => stagePage * pageSize < filteredStages.length, [stagePage, filteredStages])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setStagePage(1)
|
|
||||||
}, [stageSearchTerm])
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ce_config) return
|
if (!ce_config) return
|
||||||
if (!extraData || !extraData.theory_craft?.mode) return
|
if (!extraData || !extraData.theory_craft?.mode) return
|
||||||
@@ -110,27 +119,26 @@ export default function CeBar() {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="z-4 py-8 h-full w-full" onClick={() => {
|
<div className="z-4 pt-4 pb-2 h-full w-full space-y-4" onClick={() => {
|
||||||
|
|
||||||
setShowSearchWaveId(null)
|
setShowSearchWaveId(null)
|
||||||
setShowSearchStage(false)
|
setShowSearchStage(false)
|
||||||
}}>
|
}}>
|
||||||
|
|
||||||
<div className="mb-4 w-full relative">
|
<div className="w-full relative">
|
||||||
<div className="flex items-center justify-center gap-2">
|
<div className="flex items-center justify-center gap-2">
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline w-full text-left flex items-center gap-2"
|
className="btn btn-outline btn-warning w-full text-left flex items-center gap-2.5 rounded-xl shadow-xs"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setShowSearchStage(!showSearchStage)
|
setShowSearchStage(!showSearchStage)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Search className="w-6 h-6" />
|
<Search className="w-4 h-4" />
|
||||||
<span className="text-left"> {transI18n("stage")}: {stageList.find((s) => s.id === ce_config.stage_id.toString())?.name || transI18n("selectStage")}</span>
|
<span className="text-left font-medium"> {transI18n("stage")}: {stageList.find((s) => s.id === ce_config.stage_id.toString())?.name || transI18n("selectStage")}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{showSearchStage && (
|
{showSearchStage && (
|
||||||
<div onClick={(e) => e.stopPropagation()} className="absolute top-full mt-2 w-full z-50 border bg-base-200 border-slate-600 rounded-lg p-4 shadow-lg">
|
<div onClick={(e) => e.stopPropagation()} className="absolute top-full mt-2 w-full z-50 border bg-base-200 border-base-content/20 rounded-xl p-4 shadow-xl">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
|
||||||
<label className="input w-full">
|
<label className="input w-full">
|
||||||
@@ -147,46 +155,44 @@ export default function CeBar() {
|
|||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
<input
|
<input
|
||||||
type="search" className="grow"
|
|
||||||
placeholder={transI18n("searchStage")}
|
|
||||||
value={stageSearchTerm}
|
value={stageSearchTerm}
|
||||||
onChange={(e) => setStageSearchTerm(e.target.value)}
|
onChange={(e) => {
|
||||||
autoFocus
|
setStageSearchTerm(e.target.value)
|
||||||
|
setStagePage(1)
|
||||||
|
}}
|
||||||
|
type="search"
|
||||||
|
required
|
||||||
|
placeholder={transI18n("searchStage")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 max-h-[48vh] overflow-y-auto pr-1">
|
||||||
<div className="max-h-60 overflow-y-auto space-y-1">
|
|
||||||
{paginatedStages.length > 0 ? (
|
{paginatedStages.length > 0 ? (
|
||||||
<>
|
paginatedStages.map((stage) => (
|
||||||
{paginatedStages.map((stage) => (
|
<button
|
||||||
<div
|
key={stage.id}
|
||||||
key={stage.id}
|
className="btn btn-sm btn-outline justify-start"
|
||||||
className="p-2 hover:bg-primary/20 rounded cursor-pointer"
|
onClick={(e) => {
|
||||||
onClick={() => {
|
e.stopPropagation()
|
||||||
if (ce_config.stage_id !== Number(stage.id)) {
|
if (ce_config.stage_id !== Number(stage.id)) {
|
||||||
setCeConfig({ ...ce_config, stage_id: Number(stage.id), cycle_count: 30 })
|
setCeConfig({ ...ce_config, stage_id: Number(stage.id), cycle_count: 30 })
|
||||||
}
|
}
|
||||||
setShowSearchStage(false)
|
setShowSearchStage(false)
|
||||||
setStageSearchTerm("")
|
setStageSearchTerm("")
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{stage.name}
|
<span className="truncate">{stage.name}</span>
|
||||||
</div>
|
</button>
|
||||||
))}
|
))
|
||||||
|
|
||||||
|
|
||||||
</>
|
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm text-center py-4">{transI18n("noStageFound")}</div>
|
<div className="text-sm text-center py-4">{transI18n("noStageFound")}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{paginatedStages.length > 0 && (
|
{paginatedStages.length > 0 && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mt-4">
|
||||||
<button
|
<button
|
||||||
disabled={stagePage === 1}
|
disabled={stagePage === 1}
|
||||||
className="btn btn-sm btn-outline btn-success mt-2"
|
className="btn btn-sm btn-outline btn-success"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setStagePage(stagePage - 1)
|
setStagePage(stagePage - 1)
|
||||||
@@ -197,7 +203,7 @@ export default function CeBar() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
disabled={!hasMorePages}
|
disabled={!hasMorePages}
|
||||||
className="btn btn-sm btn-outline btn-success mt-2"
|
className="btn btn-sm btn-outline btn-success"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setStagePage(stagePage + 1)
|
setStagePage(stagePage + 1)
|
||||||
@@ -211,12 +217,12 @@ export default function CeBar() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative max-w-7xl mx-auto space-y-6">
|
<div className="relative w-full space-y-4">
|
||||||
{ce_config.monsters.map((wave, waveIndex) => (
|
{ce_config.monsters.map((wave, waveIndex) => (
|
||||||
<div key={waveIndex} className="card border border-slate-700/50 ">
|
<div key={waveIndex} className="rounded-xl border border-warning p-4 bg-base-100">
|
||||||
<div className="card-body p-6">
|
<div className="w-full">
|
||||||
<div className="flex items-center flex-wrap justify-between mb-4">
|
<div className="flex items-center flex-wrap justify-between mb-4">
|
||||||
<h2 className="text-xl font-bold text-white">{transI18n("wave")} {waveIndex + 1}</h2>
|
<h2 className="text-xl font-bold text-info">{transI18n("wave")} {waveIndex + 1}</h2>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
disabled={waveIndex === 0}
|
disabled={waveIndex === 0}
|
||||||
@@ -430,7 +436,10 @@ export default function CeBar() {
|
|||||||
type="search" className="grow"
|
type="search" className="grow"
|
||||||
placeholder={transI18n("searchMonster")}
|
placeholder={transI18n("searchMonster")}
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => {
|
||||||
|
setSearchTerm(e.target.value)
|
||||||
|
setMonsterPage(1)
|
||||||
|
}}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -513,20 +522,17 @@ export default function CeBar() {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Add New Wave Button */}
|
{/* Add New Wave Button */}
|
||||||
<div className="card border border-slate-700/50 border-dashed">
|
<button
|
||||||
<div className="card-body p-8">
|
onClick={() => {
|
||||||
<button
|
const newCeConfig = structuredClone(ce_config)
|
||||||
onClick={() => {
|
newCeConfig.monsters.push([])
|
||||||
const newCeConfig = structuredClone(ce_config)
|
setCeConfig(newCeConfig)
|
||||||
newCeConfig.monsters.push([])
|
}}
|
||||||
setCeConfig(newCeConfig)
|
className="btn btn-outline btn-warning w-full flex items-center justify-center gap-2 rounded-xl shadow-xs font-semibold"
|
||||||
}}
|
>
|
||||||
className="btn btn-primary btn-lg w-full">
|
<Plus className="w-4 h-4" />
|
||||||
<Plus className="w-6 h-6 mr-2" />
|
<span>{transI18n("addNewWave")}</span>
|
||||||
{transI18n("addNewWave")}
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,9 +6,15 @@ import AsBar from "./as";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import CeBar from "./ce";
|
import CeBar from "./ce";
|
||||||
import PeakBar from "./peak";
|
import PeakBar from "./peak";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
|
||||||
export default function MonsterBar() {
|
export default function MonsterBar() {
|
||||||
const { battle_type, setBattleType } = useUserDataStore()
|
const { battle_type, setBattleType } = useUserDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
battle_type: state.battle_type,
|
||||||
|
setBattleType: state.setBattleType,
|
||||||
|
}))
|
||||||
|
)
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
@@ -23,9 +29,9 @@ export default function MonsterBar() {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="w-full">
|
||||||
{/* Header Navigation */}
|
{/* Header Navigation */}
|
||||||
<nav className="border-b border-warning/30 relative pb-2">
|
<nav className="border-b border-base-content/10 relative pb-3">
|
||||||
<div className="flex items-center justify-center">
|
<div className="flex items-center justify-center">
|
||||||
{/* Mobile Select */}
|
{/* Mobile Select */}
|
||||||
<div className="block md:hidden w-full">
|
<div className="block md:hidden w-full">
|
||||||
@@ -43,38 +49,48 @@ export default function MonsterBar() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop Tabs */}
|
{/* Desktop Tabs */}
|
||||||
<div className="hidden md:grid grid-cols-3 lg:grid-cols-6 gap-1">
|
<div className="hidden md:grid grid-cols-3 lg:grid-cols-6 gap-2 w-full">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => {
|
||||||
<button
|
const isSelected = battle_type.toUpperCase() === item.value.toUpperCase()
|
||||||
key={item.name}
|
return (
|
||||||
onClick={() => setBattleType(item.value.toUpperCase())}
|
<button
|
||||||
className={`px-4 py-2 rounded-lg transition-all cursor-pointer duration-300 flex items-center space-x-2 ${battle_type.toUpperCase() === item.value.toUpperCase()
|
key={item.name}
|
||||||
? 'bg-success/30 shadow-lg'
|
onClick={() => setBattleType(item.value.toUpperCase())}
|
||||||
: 'bg-base-200/20 hover:bg-base-200/40 '
|
className={`px-4 py-2.5 rounded-xl transition-all cursor-pointer duration-300 flex items-center justify-center space-x-2 text-sm font-semibold border ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-success/20 border-warning shadow-lg text-base-content'
|
||||||
|
: 'bg-base-200/20 hover:bg-base-200/40 border-transparent text-base-content/80'
|
||||||
}`}
|
}`}
|
||||||
>
|
|
||||||
<span
|
|
||||||
style={
|
|
||||||
battle_type.toUpperCase() === item.value.toUpperCase()
|
|
||||||
? {
|
|
||||||
filter:
|
|
||||||
'brightness(0) saturate(100%) invert(63%) sepia(78%) saturate(643%) hue-rotate(1deg) brightness(93%) contrast(89%)',
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Image
|
<span
|
||||||
unoptimized
|
className={
|
||||||
crossOrigin="anonymous"
|
isSelected
|
||||||
src={`/icon/${item.icon}.webp`}
|
? ""
|
||||||
alt={item.name}
|
: "theme-path-icon"
|
||||||
width={24}
|
}
|
||||||
height={24}
|
style={
|
||||||
/>
|
isSelected
|
||||||
</span>
|
? {
|
||||||
<span>{item.name}</span>
|
filter:
|
||||||
</button>
|
'brightness(0) saturate(100%) invert(63%) sepia(78%) saturate(643%) hue-rotate(1deg) brightness(93%) contrast(89%)',
|
||||||
))}
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src={`/icon/${item.icon}.webp`}
|
||||||
|
alt={item.name}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
className="w-5 h-5 object-contain"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span>{item.name}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -9,14 +9,30 @@ import Image from "next/image";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { MoCEvent, MonsterStore } from "@/types";
|
import { MoCEvent, MonsterStore } from "@/types";
|
||||||
import useDetailDataStore from "@/stores/detailDataStore";
|
import useDetailDataStore from "@/stores/detailDataStore";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
|
||||||
export default function MocBar() {
|
export default function MocBar() {
|
||||||
const { locale } = useLocaleStore()
|
const { locale } = useLocaleStore(
|
||||||
|
useShallow((state) => ({ locale: state.locale }))
|
||||||
|
)
|
||||||
const {
|
const {
|
||||||
moc_config,
|
moc_config,
|
||||||
setMocConfig
|
setMocConfig
|
||||||
} = useUserDataStore()
|
} = useUserDataStore(
|
||||||
const { mapMonster, mapMoc, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
|
useShallow((state) => ({
|
||||||
|
moc_config: state.moc_config,
|
||||||
|
setMocConfig: state.setMocConfig,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { mapMonster, mapMoc, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
mapMonster: state.mapMonster,
|
||||||
|
mapMoc: state.mapMoc,
|
||||||
|
damageType: state.damageType,
|
||||||
|
hardLevelConfig: state.hardLevelConfig,
|
||||||
|
eliteConfig: state.eliteConfig,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
|
|
||||||
@@ -86,6 +102,14 @@ export default function MocBar() {
|
|||||||
|
|
||||||
if (targetEventList.length > 0) {
|
if (targetEventList.length > 0) {
|
||||||
newBattleConfig.stage_id = targetEventList[0].ID
|
newBattleConfig.stage_id = targetEventList[0].ID
|
||||||
|
|
||||||
|
if (targetEventList[0].Invasion) {
|
||||||
|
newBattleConfig.blessings.push({
|
||||||
|
id: targetEventList[0].Invasion.MazeBuff.ID,
|
||||||
|
level: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for (const wave of targetEventList[0].MonsterList) {
|
for (const wave of targetEventList[0].MonsterList) {
|
||||||
const newWave: MonsterStore[] = []
|
const newWave: MonsterStore[] = []
|
||||||
for (const value of Object.values(wave)) {
|
for (const value of Object.values(wave)) {
|
||||||
@@ -115,10 +139,10 @@ export default function MocBar() {
|
|||||||
if (!mapMoc) return null
|
if (!mapMoc) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-8 relative">
|
<div className="pt-4 pb-2 relative space-y-4">
|
||||||
|
|
||||||
{/* Title Card */}
|
{/* Title Card */}
|
||||||
<div className="rounded-xl p-4 mb-2 border border-warning">
|
<div className="rounded-xl p-4 border border-warning">
|
||||||
<div className="mb-4 w-full">
|
<div className="mb-4 w-full">
|
||||||
<SelectCustomText
|
<SelectCustomText
|
||||||
customSet={Object.values(mapMoc).sort((a, b) => b.ID - a.ID).map((moc) => ({
|
customSet={Object.values(mapMoc).sort((a, b) => b.ID - a.ID).map((moc) => ({
|
||||||
@@ -197,8 +221,8 @@ export default function MocBar() {
|
|||||||
<div className="label-text font-bold text-success mb-2">StageId: {moc_config?.stage_id}</div>
|
<div className="label-text font-bold text-success mb-2">StageId: {moc_config?.stage_id}</div>
|
||||||
|
|
||||||
{/* Turbulence Buff */}
|
{/* Turbulence Buff */}
|
||||||
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
|
<div className="pt-3 border-t border-base-content/10 space-y-2">
|
||||||
<div className="flex items-center space-x-2 mb-2">
|
<div className="flex items-center space-x-2">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={moc_config.use_turbulence_buff}
|
checked={moc_config.use_turbulence_buff}
|
||||||
@@ -215,7 +239,7 @@ export default function MocBar() {
|
|||||||
challengeSelected.MazeBuff.map((buff, i) => (
|
challengeSelected.MazeBuff.map((buff, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-base"
|
className="text-sm text-base-content/85 leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: replaceByParam(
|
__html: replaceByParam(
|
||||||
getLocaleName(locale, buff?.Desc) || "",
|
getLocaleName(locale, buff?.Desc) || "",
|
||||||
@@ -225,7 +249,7 @@ export default function MocBar() {
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
|
<div className="text-sm text-base-content/50 italic">{transI18n("noTurbulenceBuff")}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -246,7 +270,7 @@ export default function MocBar() {
|
|||||||
const targetEvent = eventList[0];
|
const targetEvent = eventList[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
|
<div key={i} className="rounded-xl p-4 border border-warning">
|
||||||
<h2 className="text-2xl font-bold mb-6 text-info">{side.wave}</h2>
|
<h2 className="text-2xl font-bold mb-6 text-info">{side.wave}</h2>
|
||||||
|
|
||||||
{targetEvent?.MonsterList?.map((wave, waveIndex) => (
|
{targetEvent?.MonsterList?.map((wave, waveIndex) => (
|
||||||
|
|||||||
+377
-285
@@ -1,308 +1,400 @@
|
|||||||
"use client"
|
"use client";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import SelectCustomText from "../select/customSelectText";
|
import SelectCustomText from "../select/customSelectText";
|
||||||
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
|
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
|
||||||
import useLocaleStore from "@/stores/localeStore";
|
import useLocaleStore from "@/stores/localeStore";
|
||||||
import useUserDataStore from "@/stores/userDataStore";;
|
import useUserDataStore from "@/stores/userDataStore";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { MonsterStore } from "@/types";
|
import { MonsterStore } from "@/types";
|
||||||
import useDetailDataStore from "@/stores/detailDataStore";
|
import useDetailDataStore from "@/stores/detailDataStore";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
|
||||||
export default function PeakBar() {
|
export default function PeakBar() {
|
||||||
const { locale } = useLocaleStore()
|
const { locale } = useLocaleStore(
|
||||||
const {
|
useShallow((state) => ({ locale: state.locale })),
|
||||||
peak_config,
|
);
|
||||||
setPeakConfig
|
const { peak_config, setPeakConfig } = useUserDataStore(
|
||||||
} = useUserDataStore()
|
useShallow((state) => ({
|
||||||
const { mapMonster, mapPeak, damageType, eliteConfig, hardLevelConfig } = useDetailDataStore()
|
peak_config: state.peak_config,
|
||||||
const transI18n = useTranslations("DataPage")
|
setPeakConfig: state.setPeakConfig,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
const { mapMonster, mapPeak, damageType, eliteConfig, hardLevelConfig } =
|
||||||
|
useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
mapMonster: state.mapMonster,
|
||||||
|
mapPeak: state.mapPeak,
|
||||||
|
damageType: state.damageType,
|
||||||
|
eliteConfig: state.eliteConfig,
|
||||||
|
hardLevelConfig: state.hardLevelConfig,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
const transI18n = useTranslations("DataPage");
|
||||||
|
|
||||||
const listFloor = useMemo(() => {
|
const listFloor = useMemo(() => {
|
||||||
const peak = mapPeak?.[peak_config?.event_id?.toString()]
|
const peak = mapPeak?.[peak_config?.event_id?.toString()];
|
||||||
if (!peak) return []
|
if (!peak) return [];
|
||||||
|
|
||||||
return [...peak.PreLevel, peak.BossLevel].filter(it => it != null)
|
return [...peak.PreLevel, peak.BossLevel].filter((it) => it != null);
|
||||||
}, [peak_config, mapPeak])
|
}, [peak_config, mapPeak]);
|
||||||
const eventSelected = useMemo(() => {
|
const eventSelected = useMemo(() => {
|
||||||
return mapPeak?.[peak_config?.event_id?.toString()]
|
return mapPeak?.[peak_config?.event_id?.toString()];
|
||||||
}, [peak_config, mapPeak])
|
}, [peak_config, mapPeak]);
|
||||||
|
|
||||||
const bossConfig = useMemo(() => {
|
const bossConfig = useMemo(() => {
|
||||||
return mapPeak?.[peak_config?.event_id?.toString()]?.BossConfig;
|
return mapPeak?.[peak_config?.event_id?.toString()]?.BossConfig;
|
||||||
}, [peak_config, mapPeak])
|
}, [peak_config, mapPeak]);
|
||||||
|
|
||||||
const challengeSelected = useMemo(() => {
|
const challengeSelected = useMemo(() => {
|
||||||
const challenge = structuredClone(listFloor?.find((peak) => peak?.ID === peak_config.challenge_id))
|
const challenge = structuredClone(
|
||||||
if (
|
listFloor?.find((peak) => peak?.ID === peak_config.challenge_id),
|
||||||
challenge
|
);
|
||||||
&& challenge.ID === mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID
|
if (
|
||||||
&& bossConfig
|
challenge &&
|
||||||
&& peak_config?.boss_mode === "Hard"
|
challenge.ID ===
|
||||||
) {
|
mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID &&
|
||||||
return { challenge: bossConfig, isBoss: true }
|
bossConfig &&
|
||||||
|
peak_config?.boss_mode === "Hard"
|
||||||
|
) {
|
||||||
|
return { challenge: bossConfig, isBoss: true };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
challenge: challenge,
|
||||||
|
isBoss:
|
||||||
|
challenge?.ID ===
|
||||||
|
mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID,
|
||||||
|
};
|
||||||
|
}, [peak_config, listFloor, mapPeak, bossConfig]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!challengeSelected.challenge) return;
|
||||||
|
if (
|
||||||
|
peak_config.event_id !== 0 &&
|
||||||
|
peak_config.challenge_id !== 0 &&
|
||||||
|
challengeSelected
|
||||||
|
) {
|
||||||
|
const newBattleConfig = structuredClone(peak_config);
|
||||||
|
newBattleConfig.cycle_count = 6;
|
||||||
|
newBattleConfig.blessings = [];
|
||||||
|
for (const value of challengeSelected?.challenge?.MazeBuff) {
|
||||||
|
newBattleConfig.blessings.push({
|
||||||
|
id: value.ID,
|
||||||
|
level: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (peak_config.buff_id !== 0 && challengeSelected.isBoss) {
|
||||||
|
newBattleConfig.blessings.push({
|
||||||
|
id: peak_config.buff_id,
|
||||||
|
level: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
newBattleConfig.monsters = [];
|
||||||
|
newBattleConfig.stage_id = challengeSelected.challenge.EventList[0].ID;
|
||||||
|
|
||||||
|
if (challengeSelected.challenge.EventList[0].Invasion) {
|
||||||
|
console.log("Adding Invasion Buff", challengeSelected.challenge.EventList[0].Invasion.MazeBuff.ID);
|
||||||
|
newBattleConfig.blessings.push({
|
||||||
|
id: challengeSelected.challenge.EventList[0].Invasion.MazeBuff.ID,
|
||||||
|
level: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const wave of challengeSelected.challenge.EventList[0].MonsterList) {
|
||||||
|
if (!wave) continue;
|
||||||
|
const newWave: MonsterStore[] = [];
|
||||||
|
for (const value of Object.values(wave)) {
|
||||||
|
if (!value) continue;
|
||||||
|
newWave.push({
|
||||||
|
monster_id: value,
|
||||||
|
level: challengeSelected.challenge.EventList[0].Level,
|
||||||
|
amount: 1,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return {
|
newBattleConfig.monsters.push(newWave);
|
||||||
challenge: challenge,
|
}
|
||||||
isBoss: challenge?.ID === mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID,
|
|
||||||
}
|
|
||||||
}, [peak_config, listFloor, mapPeak, bossConfig])
|
|
||||||
|
|
||||||
useEffect(() => {
|
setPeakConfig(newBattleConfig);
|
||||||
if (!challengeSelected.challenge) return
|
}
|
||||||
if (peak_config.event_id !== 0 && peak_config.challenge_id !== 0 && challengeSelected) {
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
const newBattleConfig = structuredClone(peak_config)
|
}, [
|
||||||
newBattleConfig.cycle_count = 6
|
peak_config.event_id,
|
||||||
newBattleConfig.blessings = []
|
peak_config.challenge_id,
|
||||||
for (const value of challengeSelected?.challenge?.MazeBuff) {
|
peak_config.buff_id,
|
||||||
newBattleConfig.blessings.push({
|
peak_config.boss_mode,
|
||||||
id: value.ID,
|
mapPeak,
|
||||||
level: 1
|
]);
|
||||||
})
|
|
||||||
|
if (!mapPeak) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pt-4 pb-2 relative space-y-4">
|
||||||
|
{/* Title Card */}
|
||||||
|
<div className="rounded-xl p-4 border border-warning">
|
||||||
|
<div className="mb-4 w-full">
|
||||||
|
<SelectCustomText
|
||||||
|
customSet={Object.values(mapPeak)
|
||||||
|
.sort((a, b) => b.ID - a.ID)
|
||||||
|
.map((peak) => ({
|
||||||
|
id: peak.ID.toString(),
|
||||||
|
name: `${getLocaleName(locale, peak.Name)} (${peak.ID})`,
|
||||||
|
}))}
|
||||||
|
excludeSet={[]}
|
||||||
|
selectedCustomSet={peak_config.event_id.toString()}
|
||||||
|
placeholder={transI18n("selectPEAKEvent")}
|
||||||
|
setSelectedCustomSet={(id) =>
|
||||||
|
setPeakConfig({
|
||||||
|
...peak_config,
|
||||||
|
event_id: Number(id),
|
||||||
|
challenge_id: 0,
|
||||||
|
buff_id: 0,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if (peak_config.buff_id !== 0 && challengeSelected.isBoss) {
|
/>
|
||||||
newBattleConfig.blessings.push({
|
</div>
|
||||||
id: peak_config.buff_id,
|
{/* Settings */}
|
||||||
level: 1
|
<div
|
||||||
})
|
className={`grid grid-cols-1
|
||||||
}
|
|
||||||
newBattleConfig.monsters = []
|
|
||||||
newBattleConfig.stage_id = challengeSelected.challenge.EventList[0].ID
|
|
||||||
for (const wave of challengeSelected.challenge.EventList[0].MonsterList) {
|
|
||||||
if (!wave) continue
|
|
||||||
const newWave: MonsterStore[] = []
|
|
||||||
for (const value of Object.values(wave)) {
|
|
||||||
if (!value) continue
|
|
||||||
newWave.push({
|
|
||||||
monster_id: value,
|
|
||||||
level: challengeSelected.challenge.EventList[0].Level,
|
|
||||||
amount: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
newBattleConfig.monsters.push(newWave)
|
|
||||||
}
|
|
||||||
|
|
||||||
setPeakConfig(newBattleConfig)
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [
|
|
||||||
peak_config.event_id,
|
|
||||||
peak_config.challenge_id,
|
|
||||||
peak_config.buff_id,
|
|
||||||
peak_config.boss_mode,
|
|
||||||
mapPeak,
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!mapPeak) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="py-8 relative">
|
|
||||||
|
|
||||||
{/* Title Card */}
|
|
||||||
<div className="rounded-xl p-4 mb-2 border border-warning">
|
|
||||||
<div className="mb-4 w-full">
|
|
||||||
<SelectCustomText
|
|
||||||
customSet={Object.values(mapPeak).sort((a, b) => b.ID - a.ID).map((peak) => ({
|
|
||||||
id: peak.ID.toString(),
|
|
||||||
name: `${getLocaleName(locale, peak.Name)} (${peak.ID})`,
|
|
||||||
}))}
|
|
||||||
excludeSet={[]}
|
|
||||||
selectedCustomSet={peak_config.event_id.toString()}
|
|
||||||
placeholder={transI18n("selectPEAKEvent")}
|
|
||||||
setSelectedCustomSet={(id) => setPeakConfig({ ...peak_config, event_id: Number(id), challenge_id: 0, buff_id: 0 })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/* Settings */}
|
|
||||||
<div className={
|
|
||||||
`grid grid-cols-1
|
|
||||||
${eventSelected && eventSelected.BossLevel?.ID === peak_config.challenge_id ? "md:grid-cols-2" : ""}
|
${eventSelected && eventSelected.BossLevel?.ID === peak_config.challenge_id ? "md:grid-cols-2" : ""}
|
||||||
gap-4 mb-2 justify-items-center items-center w-full`}
|
gap-4 mb-2 justify-items-center items-center w-full`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-bold text-success">
|
||||||
|
{transI18n("floor")}:{" "}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={peak_config.challenge_id}
|
||||||
|
className="select select-success w-full"
|
||||||
|
onChange={(e) =>
|
||||||
|
setPeakConfig({
|
||||||
|
...peak_config,
|
||||||
|
challenge_id: Number(e.target.value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value={0} disabled={true}>
|
||||||
|
{transI18n("selectFloor")}
|
||||||
|
</option>
|
||||||
|
{listFloor.map((peak) => (
|
||||||
|
<option key={peak.ID} value={peak.ID}>
|
||||||
|
{getLocaleName(locale, peak.Name)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{eventSelected &&
|
||||||
|
eventSelected.BossLevel?.ID === peak_config.challenge_id && (
|
||||||
|
<div className="flex items-center gap-2 w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-bold text-success">
|
||||||
|
{transI18n("mode")}:{" "}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={peak_config.boss_mode}
|
||||||
|
className="select select-success w-full"
|
||||||
|
onChange={(e) =>
|
||||||
|
setPeakConfig({ ...peak_config, boss_mode: e.target.value })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
|
<option value={0} disabled={true}>
|
||||||
<div className="flex items-center gap-2 w-full">
|
{transI18n("selectSide")}
|
||||||
<label className="label">
|
</option>
|
||||||
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
|
<option value="Normal">{transI18n("normalMode")}</option>
|
||||||
</label>
|
<option value="Hard">{transI18n("hardMode")}</option>
|
||||||
<select
|
</select>
|
||||||
value={peak_config.challenge_id}
|
</div>
|
||||||
className="select select-success w-full"
|
|
||||||
onChange={(e) => setPeakConfig({ ...peak_config, challenge_id: Number(e.target.value) })}
|
|
||||||
>
|
|
||||||
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
|
|
||||||
{listFloor.map((peak) => (
|
|
||||||
<option key={peak.ID} value={peak.ID}>{getLocaleName(locale, peak.Name)}</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{eventSelected && eventSelected.BossLevel?.ID === peak_config.challenge_id && (
|
|
||||||
<div className="flex items-center gap-2 w-full">
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-bold text-success">{transI18n("mode")}:{" "}</span>
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
value={peak_config.boss_mode}
|
|
||||||
className="select select-success w-full"
|
|
||||||
onChange={(e) => setPeakConfig({ ...peak_config, boss_mode: e.target.value })}
|
|
||||||
>
|
|
||||||
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
|
|
||||||
<option value="Normal">{transI18n("normalMode")}</option>
|
|
||||||
<option value="Hard">{transI18n("hardMode")}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div className="label-text font-bold text-success mb-2">StageId: {peak_config?.stage_id}</div>
|
|
||||||
{
|
|
||||||
eventSelected
|
|
||||||
&& eventSelected.BossLevel?.ID === peak_config.challenge_id
|
|
||||||
&& bossConfig
|
|
||||||
&& (
|
|
||||||
<div className="mb-4 w-full">
|
|
||||||
<SelectCustomText
|
|
||||||
customSet={
|
|
||||||
bossConfig.BuffList.map((buff) => ({
|
|
||||||
id: buff.ID.toString(),
|
|
||||||
name: getLocaleName(locale, buff?.Name || ""),
|
|
||||||
description: replaceByParam(getLocaleName(locale, buff?.Desc || ""), buff?.Param || []),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
excludeSet={[]}
|
|
||||||
selectedCustomSet={peak_config?.buff_id?.toString()}
|
|
||||||
placeholder={transI18n("selectBuff")}
|
|
||||||
setSelectedCustomSet={(id) => setPeakConfig({ ...peak_config, buff_id: Number(id) })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
{/* Turbulence Buff */}
|
|
||||||
|
|
||||||
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
|
|
||||||
<h2 className="text-2xl font-bold mb-2 text-info">
|
|
||||||
{transI18n("turbulenceBuff")}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{challengeSelected?.challenge && challengeSelected?.challenge?.MazeBuff?.length > 0 ? (
|
|
||||||
challengeSelected.challenge.MazeBuff.map((subOption, index) => (
|
|
||||||
<div key={index}>
|
|
||||||
<label className="label">
|
|
||||||
<span className="label-text font-bold text-success">
|
|
||||||
{index + 1}. {getLocaleName(locale, subOption.Name)}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<div
|
|
||||||
className="text-base"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: replaceByParam(
|
|
||||||
getLocaleName(locale, subOption.Desc),
|
|
||||||
subOption.Param || []
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Enemy Waves */}
|
|
||||||
|
|
||||||
{(peak_config?.challenge_id ?? 0) !== 0 && (
|
|
||||||
<div className="grid grid-cols-1 gap-4">
|
|
||||||
|
|
||||||
<div className="rounded-xl p-4 mt-2 border border-warning">
|
|
||||||
<h2 className="text-2xl font-bold mb-2 text-info">{getLocaleName(locale, challengeSelected?.challenge?.Name)}</h2>
|
|
||||||
|
|
||||||
|
|
||||||
{challengeSelected?.challenge && Object.values(challengeSelected?.challenge?.EventList?.[0]?.Infinite || []).map((waveValue, waveIndex) => (
|
|
||||||
<div key={waveIndex} className="mb-6">
|
|
||||||
<h3 className="text-lg font-semibold">{transI18n("wave")} {waveIndex + 1}</h3>
|
|
||||||
<div className="flex flex-wrap gap-2 mt-2">
|
|
||||||
{Array.from(new Set(waveValue.MonsterList)).map((monsterId, enemyIndex) => {
|
|
||||||
const monsterStats = calcMonsterStats(
|
|
||||||
mapMonster?.[monsterId.toString()],
|
|
||||||
waveValue.EliteGroup,
|
|
||||||
challengeSelected?.challenge?.EventList?.[0]?.HardLevelGroup || 0,
|
|
||||||
challengeSelected?.challenge?.EventList?.[0]?.Level || 0,
|
|
||||||
hardLevelConfig,
|
|
||||||
eliteConfig
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={enemyIndex}
|
|
||||||
className="group relative flex flex-col w-40 bg-base-100 rounded-2xl border border-base-300 shadow-md"
|
|
||||||
>
|
|
||||||
<div className="badge badge-warning badge-sm font-bold absolute top-2 right-2 z-10 shadow-sm">
|
|
||||||
Lv. {challengeSelected?.challenge?.EventList?.[0]?.Level}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
|
|
||||||
{mapMonster?.[monsterId.toString()]?.Image?.IconPath && (
|
|
||||||
<div className="relative w-16 h-16 rounded-full border-2 border-base-300 shadow-md overflow-hidden group-hover:scale-110 transition-transform duration-300 bg-base-100">
|
|
||||||
<Image
|
|
||||||
unoptimized
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
src={`${process.env.CDN_URL}/${mapMonster?.[monsterId.toString()]?.Image?.IconPath}`}
|
|
||||||
alt="Enemy Icon"
|
|
||||||
width={150}
|
|
||||||
height={150}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col px-1 pb-2 pt-2">
|
|
||||||
<div className="flex flex-col space-y-1.5">
|
|
||||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
|
||||||
<span className="text-xs font-semibold text-error">HP</span>
|
|
||||||
<span className="text-sm font-bold text-base-content">{monsterStats.hp.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
|
||||||
<span className="text-xs font-semibold text-info">Speed</span>
|
|
||||||
<span className="text-sm font-bold text-base-content">{monsterStats.spd.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
|
||||||
<span className="text-xs font-semibold text-base-content/70">Toughness</span>
|
|
||||||
<span className="text-sm font-bold text-base-content">{monsterStats.stance.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-2 pt-2 border-t border-base-300 flex flex-col items-center">
|
|
||||||
<span className="text-[10px] text-base-content/60 font-bold uppercase tracking-widest mb-1.5">
|
|
||||||
Weakness
|
|
||||||
</span>
|
|
||||||
<div className="flex items-center justify-center gap-1.5 flex-wrap">
|
|
||||||
{mapMonster?.[monsterId.toString()]?.StanceWeakList?.map((icon, iconIndex) => (
|
|
||||||
<Image
|
|
||||||
key={iconIndex}
|
|
||||||
unoptimized
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
src={`${process.env.CDN_URL}/${damageType[icon]?.Icon}`}
|
|
||||||
alt={icon}
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
className="h-6 w-6 object-contain rounded-full bg-base-300 border border-base-content/10 p-0.5 shadow-sm hover:scale-110 transition-transform"
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
<div className="label-text font-bold text-success mb-2">
|
||||||
|
StageId: {peak_config?.stage_id}
|
||||||
|
</div>
|
||||||
|
{eventSelected &&
|
||||||
|
eventSelected.BossLevel?.ID === peak_config.challenge_id &&
|
||||||
|
bossConfig && (
|
||||||
|
<div className="mb-4 w-full">
|
||||||
|
<SelectCustomText
|
||||||
|
customSet={bossConfig.BuffList.map((buff) => ({
|
||||||
|
id: buff.ID.toString(),
|
||||||
|
name: getLocaleName(locale, buff?.Name || ""),
|
||||||
|
description: replaceByParam(
|
||||||
|
getLocaleName(locale, buff?.Desc || ""),
|
||||||
|
buff?.Param || [],
|
||||||
|
),
|
||||||
|
}))}
|
||||||
|
excludeSet={[]}
|
||||||
|
selectedCustomSet={peak_config?.buff_id?.toString()}
|
||||||
|
placeholder={transI18n("selectBuff")}
|
||||||
|
setSelectedCustomSet={(id) =>
|
||||||
|
setPeakConfig({ ...peak_config, buff_id: Number(id) })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Turbulence Buff */}
|
||||||
|
<div className="pt-3 border-t border-base-content/10 space-y-2">
|
||||||
|
<h2 className="text-xl font-bold text-info">
|
||||||
|
{transI18n("turbulenceBuff")}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{challengeSelected?.challenge &&
|
||||||
|
challengeSelected?.challenge?.MazeBuff?.length > 0 ? (
|
||||||
|
challengeSelected.challenge.MazeBuff.map((subOption, index) => (
|
||||||
|
<div key={index} className="space-y-1">
|
||||||
|
<span className="font-bold text-success text-sm">
|
||||||
|
{index + 1}. {getLocaleName(locale, subOption.Name)}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className="text-sm text-base-content/85 leading-relaxed"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: replaceByParam(
|
||||||
|
getLocaleName(locale, subOption.Desc),
|
||||||
|
subOption.Param || [],
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-base-content/50 italic">
|
||||||
|
{transI18n("noTurbulenceBuff")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Enemy Waves */}
|
||||||
|
|
||||||
|
{(peak_config?.challenge_id ?? 0) !== 0 && (
|
||||||
|
<div className="grid grid-cols-1 gap-4">
|
||||||
|
<div className="rounded-xl p-4 border border-warning">
|
||||||
|
<h2 className="text-2xl font-bold mb-2 text-info">
|
||||||
|
{getLocaleName(locale, challengeSelected?.challenge?.Name)}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{challengeSelected?.challenge &&
|
||||||
|
Object.values(
|
||||||
|
challengeSelected?.challenge?.EventList?.[0]?.Infinite || [],
|
||||||
|
).map((waveValue, waveIndex) => (
|
||||||
|
<div key={waveIndex} className="mb-6">
|
||||||
|
<h3 className="text-lg font-semibold">
|
||||||
|
{transI18n("wave")} {waveIndex + 1}
|
||||||
|
</h3>
|
||||||
|
<div className="flex flex-wrap gap-2 mt-2">
|
||||||
|
{Array.from(new Set(waveValue.MonsterList)).map(
|
||||||
|
(monsterId, enemyIndex) => {
|
||||||
|
const monsterStats = calcMonsterStats(
|
||||||
|
mapMonster?.[monsterId.toString()],
|
||||||
|
waveValue.EliteGroup,
|
||||||
|
challengeSelected?.challenge?.EventList?.[0]
|
||||||
|
?.HardLevelGroup || 0,
|
||||||
|
challengeSelected?.challenge?.EventList?.[0]?.Level ||
|
||||||
|
0,
|
||||||
|
hardLevelConfig,
|
||||||
|
eliteConfig,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={enemyIndex}
|
||||||
|
className="group relative flex flex-col w-40 bg-base-100 rounded-2xl border border-base-300 shadow-md"
|
||||||
|
>
|
||||||
|
<div className="badge badge-warning badge-sm font-bold absolute top-2 right-2 z-10 shadow-sm">
|
||||||
|
Lv.{" "}
|
||||||
|
{
|
||||||
|
challengeSelected?.challenge?.EventList?.[0]
|
||||||
|
?.Level
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
|
||||||
|
{mapMonster?.[monsterId.toString()]?.Image
|
||||||
|
?.IconPath && (
|
||||||
|
<div className="relative w-16 h-16 rounded-full border-2 border-base-300 shadow-md overflow-hidden group-hover:scale-110 transition-transform duration-300 bg-base-100">
|
||||||
|
<Image
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src={`${process.env.CDN_URL}/${mapMonster?.[monsterId.toString()]?.Image?.IconPath}`}
|
||||||
|
alt="Enemy Icon"
|
||||||
|
width={150}
|
||||||
|
height={150}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col px-1 pb-2 pt-2">
|
||||||
|
<div className="flex flex-col space-y-1.5">
|
||||||
|
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||||
|
<span className="text-xs font-semibold text-error">
|
||||||
|
HP
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-bold text-base-content">
|
||||||
|
{monsterStats.hp.toLocaleString(undefined, {
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||||
|
<span className="text-xs font-semibold text-info">
|
||||||
|
Speed
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-bold text-base-content">
|
||||||
|
{monsterStats.spd.toLocaleString(
|
||||||
|
undefined,
|
||||||
|
{ maximumFractionDigits: 0 },
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
|
||||||
|
<span className="text-xs font-semibold text-base-content/70">
|
||||||
|
Toughness
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-bold text-base-content">
|
||||||
|
{monsterStats.stance.toLocaleString(
|
||||||
|
undefined,
|
||||||
|
{ maximumFractionDigits: 0 },
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-2 pt-2 border-t border-base-300 flex flex-col items-center">
|
||||||
|
<span className="text-[10px] text-base-content/60 font-bold uppercase tracking-widest mb-1.5">
|
||||||
|
Weakness
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center justify-center gap-1.5 flex-wrap">
|
||||||
|
{mapMonster?.[
|
||||||
|
monsterId.toString()
|
||||||
|
]?.StanceWeakList?.map((icon, iconIndex) => (
|
||||||
|
<Image
|
||||||
|
key={iconIndex}
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
src={`${process.env.CDN_URL}/${damageType[icon]?.Icon}`}
|
||||||
|
alt={icon}
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
className="h-6 w-6 object-contain rounded-full bg-base-300 border border-base-content/10 p-0.5 shadow-sm hover:scale-110 transition-transform"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -8,14 +8,30 @@ import Image from "next/image";
|
|||||||
import { MonsterStore, PFEvent } from "@/types";
|
import { MonsterStore, PFEvent } from "@/types";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import useDetailDataStore from "@/stores/detailDataStore";
|
import useDetailDataStore from "@/stores/detailDataStore";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
|
||||||
export default function PfBar() {
|
export default function PfBar() {
|
||||||
const { locale } = useLocaleStore()
|
const { locale } = useLocaleStore(
|
||||||
|
useShallow((state) => ({ locale: state.locale }))
|
||||||
|
)
|
||||||
const {
|
const {
|
||||||
pf_config,
|
pf_config,
|
||||||
setPfConfig
|
setPfConfig
|
||||||
} = useUserDataStore()
|
} = useUserDataStore(
|
||||||
const { mapMonster, mapPF, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
|
useShallow((state) => ({
|
||||||
|
pf_config: state.pf_config,
|
||||||
|
setPfConfig: state.setPfConfig,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { mapMonster, mapPF, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
mapMonster: state.mapMonster,
|
||||||
|
mapPF: state.mapPF,
|
||||||
|
damageType: state.damageType,
|
||||||
|
hardLevelConfig: state.hardLevelConfig,
|
||||||
|
eliteConfig: state.eliteConfig,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
const challengeSelected = useMemo(() => {
|
const challengeSelected = useMemo(() => {
|
||||||
@@ -89,6 +105,14 @@ export default function PfBar() {
|
|||||||
|
|
||||||
if (targetEventList.length > 0) {
|
if (targetEventList.length > 0) {
|
||||||
newBattleConfig.stage_id = targetEventList[0].ID
|
newBattleConfig.stage_id = targetEventList[0].ID
|
||||||
|
|
||||||
|
if (targetEventList[0].Invasion) {
|
||||||
|
newBattleConfig.blessings.push({
|
||||||
|
id: targetEventList[0].Invasion.MazeBuff.ID,
|
||||||
|
level: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for (const wave of targetEventList[0].MonsterList) {
|
for (const wave of targetEventList[0].MonsterList) {
|
||||||
const newWave: MonsterStore[] = []
|
const newWave: MonsterStore[] = []
|
||||||
for (const value of Object.values(wave)) {
|
for (const value of Object.values(wave)) {
|
||||||
@@ -116,10 +140,10 @@ export default function PfBar() {
|
|||||||
if (!mapPF) return null
|
if (!mapPF) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="py-8 relative">
|
<div className="pt-4 pb-2 relative space-y-4">
|
||||||
|
|
||||||
{/* Title Card */}
|
{/* Title Card */}
|
||||||
<div className="rounded-xl p-4 mb-2 border border-warning">
|
<div className="rounded-xl p-4 border border-warning">
|
||||||
<div className="mb-4 w-full">
|
<div className="mb-4 w-full">
|
||||||
<SelectCustomText
|
<SelectCustomText
|
||||||
customSet={Object.values(mapPF).sort((a, b) => b.ID - a.ID).map((pf) => ({
|
customSet={Object.values(mapPF).sort((a, b) => b.ID - a.ID).map((pf) => ({
|
||||||
@@ -190,16 +214,14 @@ export default function PfBar() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* Turbulence Buff */}
|
{/* Turbulence Buff */}
|
||||||
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
|
<div className="pt-3 border-t border-base-content/10 space-y-2">
|
||||||
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2>
|
<h2 className="text-xl font-bold text-info">{transI18n("turbulenceBuff")}</h2>
|
||||||
{eventSelected && eventSelected.SubOption.length > 0 ? (
|
{eventSelected && eventSelected.SubOption.length > 0 ? (
|
||||||
eventSelected.SubOption.map((subOption, index) => (
|
eventSelected.SubOption.map((subOption, index) => (
|
||||||
<div key={index}>
|
<div key={index} className="space-y-1">
|
||||||
<label className="label">
|
<span className="font-bold text-success text-sm">{index + 1}. {getLocaleName(locale, subOption.Name)}</span>
|
||||||
<span className="label-text font-bold text-success">{index + 1}. {getLocaleName(locale, subOption.Name)}</span>
|
|
||||||
</label>
|
|
||||||
<div
|
<div
|
||||||
className="text-base"
|
className="text-sm text-base-content/85 leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: replaceByParam(
|
__html: replaceByParam(
|
||||||
getLocaleName(locale, subOption.Desc) || "",
|
getLocaleName(locale, subOption.Desc) || "",
|
||||||
@@ -213,7 +235,7 @@ export default function PfBar() {
|
|||||||
challengeSelected?.MazeBuff?.map((buff, i) => (
|
challengeSelected?.MazeBuff?.map((buff, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="text-base"
|
className="text-sm text-base-content/85 leading-relaxed"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: replaceByParam(
|
__html: replaceByParam(
|
||||||
getLocaleName(locale, buff?.Desc) || "",
|
getLocaleName(locale, buff?.Desc) || "",
|
||||||
@@ -223,13 +245,12 @@ export default function PfBar() {
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
|
<div className="text-sm text-base-content/50 italic">{transI18n("noTurbulenceBuff")}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Enemy Waves */}
|
{/* Enemy Waves */}
|
||||||
|
|
||||||
{(pf_config?.challenge_id ?? 0) !== 0 && (
|
{(pf_config?.challenge_id ?? 0) !== 0 && (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{floorSideList.map((side, i) => {
|
{floorSideList.map((side, i) => {
|
||||||
@@ -245,7 +266,7 @@ export default function PfBar() {
|
|||||||
const targetEvent = eventList[0];
|
const targetEvent = eventList[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={i} className="rounded-xl p-4 mt-2 border border-warning">
|
<div key={i} className="rounded-xl p-4 border border-warning">
|
||||||
<h2 className="text-2xl font-bold mb-2 text-info">{side.wave}</h2>
|
<h2 className="text-2xl font-bold mb-2 text-info">{side.wave}</h2>
|
||||||
|
|
||||||
{targetEvent && Object.values(targetEvent.Infinite || []).map((waveValue, waveIndex) => (
|
{targetEvent && Object.values(targetEvent.Infinite || []).map((waveValue, waveIndex) => (
|
||||||
|
|||||||
@@ -9,14 +9,28 @@ import RelicShowcase from "../showcaseCard/relicShowcase";
|
|||||||
import useLocaleStore from "@/stores/localeStore";
|
import useLocaleStore from "@/stores/localeStore";
|
||||||
import useDetailDataStore from "@/stores/detailDataStore";
|
import useDetailDataStore from "@/stores/detailDataStore";
|
||||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
|
||||||
export default function QuickView() {
|
export default function QuickView() {
|
||||||
const { avatars } = useUserDataStore()
|
const { avatars } = useUserDataStore(
|
||||||
|
useShallow((state) => ({ avatars: state.avatars }))
|
||||||
|
)
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
const { locale } = useLocaleStore()
|
const { locale } = useLocaleStore(
|
||||||
const { avatarSelected, } = useCurrentDataStore()
|
useShallow((state) => ({ locale: state.locale }))
|
||||||
const { mainAffix, subAffix, mapRelicSet, mapLightCone, mapAvatar } = useDetailDataStore()
|
)
|
||||||
|
const { avatarSelected } = useCurrentDataStore(
|
||||||
|
useShallow((state) => ({ avatarSelected: state.avatarSelected }))
|
||||||
|
)
|
||||||
|
const { mainAffix, subAffix, mapRelicSet, mapLightCone, mapAvatar } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
mainAffix: state.mainAffix,
|
||||||
|
subAffix: state.subAffix,
|
||||||
|
mapRelicSet: state.mapRelicSet,
|
||||||
|
mapLightCone: state.mapLightCone,
|
||||||
|
mapAvatar: state.mapAvatar,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
const avatarSkillTree = useMemo(() => {
|
const avatarSkillTree = useMemo(() => {
|
||||||
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
|
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
|
||||||
@@ -33,18 +47,17 @@ export default function QuickView() {
|
|||||||
|
|
||||||
const avatarProfile = useMemo(() => {
|
const avatarProfile = useMemo(() => {
|
||||||
if (!avatarSelected || !avatarData) return
|
if (!avatarSelected || !avatarData) return
|
||||||
return avatarData?.profileList?.[avatarData?.profileSelect]
|
return avatarData?.profileList?.[avatarData?.profileSelect] ?? avatarData?.profileList?.[0]
|
||||||
}, [avatarSelected, avatarData])
|
}, [avatarSelected, avatarData])
|
||||||
|
|
||||||
const relicEffects = useMemo(() => {
|
const relicEffects = useMemo(() => {
|
||||||
const avatar = avatars[avatarSelected?.ID?.toString() || ""];
|
const avatar = avatars[avatarSelected?.ID?.toString() || ""];
|
||||||
const relicCount: { [key: string]: number } = {};
|
const relicCount: { [key: string]: number } = {};
|
||||||
if (avatar) {
|
const profile = avatar?.profileList?.[avatar?.profileSelect] ?? avatar?.profileList?.[0];
|
||||||
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
|
if (profile?.relics) {
|
||||||
if (relicCount[relic.relic_set_id]) {
|
for (const relic of Object.values(profile.relics)) {
|
||||||
relicCount[relic.relic_set_id]++;
|
if (relic && relic.relic_set_id) {
|
||||||
} else {
|
relicCount[relic.relic_set_id] = (relicCount[relic.relic_set_id] || 0) + 1;
|
||||||
relicCount[relic.relic_set_id] = 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +121,7 @@ export default function QuickView() {
|
|||||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
|
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
|
||||||
avatarData.level
|
avatarData.level
|
||||||
),
|
),
|
||||||
name: "HP",
|
name: transI18n("hp"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconMaxHP.png",
|
icon: "spriteoutput/ui/avatar/icon/IconMaxHP.png",
|
||||||
unit: "",
|
unit: "",
|
||||||
round: 0
|
round: 0
|
||||||
@@ -124,7 +137,7 @@ export default function QuickView() {
|
|||||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
|
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
|
||||||
avatarData.level
|
avatarData.level
|
||||||
),
|
),
|
||||||
name: "ATK",
|
name: transI18n("atk"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconAttack.png",
|
icon: "spriteoutput/ui/avatar/icon/IconAttack.png",
|
||||||
unit: "",
|
unit: "",
|
||||||
round: 0
|
round: 0
|
||||||
@@ -140,7 +153,7 @@ export default function QuickView() {
|
|||||||
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
|
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
|
||||||
avatarData.level
|
avatarData.level
|
||||||
),
|
),
|
||||||
name: "DEF",
|
name: transI18n("def"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconDefence.png",
|
icon: "spriteoutput/ui/avatar/icon/IconDefence.png",
|
||||||
unit: "",
|
unit: "",
|
||||||
round: 0
|
round: 0
|
||||||
@@ -148,7 +161,7 @@ export default function QuickView() {
|
|||||||
SPD: {
|
SPD: {
|
||||||
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
|
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
|
||||||
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
|
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
|
||||||
name: "SPD",
|
name: transI18n("speed"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconSpeed.png",
|
icon: "spriteoutput/ui/avatar/icon/IconSpeed.png",
|
||||||
unit: "",
|
unit: "",
|
||||||
round: 1
|
round: 1
|
||||||
@@ -156,7 +169,7 @@ export default function QuickView() {
|
|||||||
CRITRate: {
|
CRITRate: {
|
||||||
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
|
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
|
||||||
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
|
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
|
||||||
name: "CRIT Rate",
|
name: transI18n("critRate"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconCriticalChance.png",
|
icon: "spriteoutput/ui/avatar/icon/IconCriticalChance.png",
|
||||||
unit: "%",
|
unit: "%",
|
||||||
round: 1
|
round: 1
|
||||||
@@ -164,7 +177,7 @@ export default function QuickView() {
|
|||||||
CRITDmg: {
|
CRITDmg: {
|
||||||
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
|
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
|
||||||
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
|
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
|
||||||
name: "CRIT DMG",
|
name: transI18n("critDmg"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
|
icon: "spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
|
||||||
unit: "%",
|
unit: "%",
|
||||||
round: 1
|
round: 1
|
||||||
@@ -172,7 +185,7 @@ export default function QuickView() {
|
|||||||
BreakEffect: {
|
BreakEffect: {
|
||||||
value: 0,
|
value: 0,
|
||||||
base: 0,
|
base: 0,
|
||||||
name: "Break Effect",
|
name: transI18n("breakEffect"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconBreakUp.png",
|
icon: "spriteoutput/ui/avatar/icon/IconBreakUp.png",
|
||||||
unit: "%",
|
unit: "%",
|
||||||
round: 1
|
round: 1
|
||||||
@@ -180,7 +193,7 @@ export default function QuickView() {
|
|||||||
EffectRES: {
|
EffectRES: {
|
||||||
value: 0,
|
value: 0,
|
||||||
base: 0,
|
base: 0,
|
||||||
name: "Effect RES",
|
name: transI18n("effectRes"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconStatusResistance.png",
|
icon: "spriteoutput/ui/avatar/icon/IconStatusResistance.png",
|
||||||
unit: "%",
|
unit: "%",
|
||||||
round: 1
|
round: 1
|
||||||
@@ -188,7 +201,7 @@ export default function QuickView() {
|
|||||||
EnergyRate: {
|
EnergyRate: {
|
||||||
value: 0,
|
value: 0,
|
||||||
base: 0,
|
base: 0,
|
||||||
name: "Energy Rate",
|
name: transI18n("energyRegenerationRate"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
|
icon: "spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
|
||||||
unit: "%",
|
unit: "%",
|
||||||
round: 1
|
round: 1
|
||||||
@@ -196,7 +209,7 @@ export default function QuickView() {
|
|||||||
EffectHitRate: {
|
EffectHitRate: {
|
||||||
value: 0,
|
value: 0,
|
||||||
base: 0,
|
base: 0,
|
||||||
name: "Effect Hit Rate",
|
name: transI18n("effectHitRate"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconStatusProbability.png",
|
icon: "spriteoutput/ui/avatar/icon/IconStatusProbability.png",
|
||||||
unit: "%",
|
unit: "%",
|
||||||
round: 1
|
round: 1
|
||||||
@@ -204,7 +217,7 @@ export default function QuickView() {
|
|||||||
HealBoost: {
|
HealBoost: {
|
||||||
value: 0,
|
value: 0,
|
||||||
base: 0,
|
base: 0,
|
||||||
name: "Healing Boost",
|
name: transI18n("outgoingHealingBoost"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconHealRatio.png",
|
icon: "spriteoutput/ui/avatar/icon/IconHealRatio.png",
|
||||||
unit: "%",
|
unit: "%",
|
||||||
round: 1
|
round: 1
|
||||||
@@ -212,7 +225,7 @@ export default function QuickView() {
|
|||||||
PhysicalAdd: {
|
PhysicalAdd: {
|
||||||
value: 0,
|
value: 0,
|
||||||
base: 0,
|
base: 0,
|
||||||
name: "Physical Boost",
|
name: transI18n("physicalDmgBoost"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
|
icon: "spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
|
||||||
unit: "%",
|
unit: "%",
|
||||||
round: 1
|
round: 1
|
||||||
@@ -220,7 +233,7 @@ export default function QuickView() {
|
|||||||
FireAdd: {
|
FireAdd: {
|
||||||
value: 0,
|
value: 0,
|
||||||
base: 0,
|
base: 0,
|
||||||
name: "Fire Boost",
|
name: transI18n("fireDmgBoost"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
|
icon: "spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
|
||||||
unit: "%",
|
unit: "%",
|
||||||
round: 1
|
round: 1
|
||||||
@@ -228,7 +241,7 @@ export default function QuickView() {
|
|||||||
IceAdd: {
|
IceAdd: {
|
||||||
value: 0,
|
value: 0,
|
||||||
base: 0,
|
base: 0,
|
||||||
name: "Ice Boost",
|
name: transI18n("iceDmgBoost"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
|
icon: "spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
|
||||||
unit: "%",
|
unit: "%",
|
||||||
round: 1
|
round: 1
|
||||||
@@ -236,7 +249,7 @@ export default function QuickView() {
|
|||||||
ThunderAdd: {
|
ThunderAdd: {
|
||||||
value: 0,
|
value: 0,
|
||||||
base: 0,
|
base: 0,
|
||||||
name: "Thunder Boost",
|
name: transI18n("thunderDmgBoost"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
|
icon: "spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
|
||||||
unit: "%",
|
unit: "%",
|
||||||
round: 1
|
round: 1
|
||||||
@@ -244,7 +257,7 @@ export default function QuickView() {
|
|||||||
WindAdd: {
|
WindAdd: {
|
||||||
value: 0,
|
value: 0,
|
||||||
base: 0,
|
base: 0,
|
||||||
name: "Wind Boost",
|
name: transI18n("windDmgBoost"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
|
icon: "spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
|
||||||
unit: "%",
|
unit: "%",
|
||||||
round: 1
|
round: 1
|
||||||
@@ -252,7 +265,7 @@ export default function QuickView() {
|
|||||||
QuantumAdd: {
|
QuantumAdd: {
|
||||||
value: 0,
|
value: 0,
|
||||||
base: 0,
|
base: 0,
|
||||||
name: "Quantum Boost",
|
name: transI18n("quantumDmgBoost"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
|
icon: "spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
|
||||||
unit: "%",
|
unit: "%",
|
||||||
round: 1
|
round: 1
|
||||||
@@ -260,7 +273,7 @@ export default function QuickView() {
|
|||||||
ImaginaryAdd: {
|
ImaginaryAdd: {
|
||||||
value: 0,
|
value: 0,
|
||||||
base: 0,
|
base: 0,
|
||||||
name: "Imaginary Boost",
|
name: transI18n("imaginaryDmgBoost"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
|
icon: "spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
|
||||||
unit: "%",
|
unit: "%",
|
||||||
round: 1
|
round: 1
|
||||||
@@ -268,7 +281,7 @@ export default function QuickView() {
|
|||||||
ElationAdd: {
|
ElationAdd: {
|
||||||
value: 0,
|
value: 0,
|
||||||
base: 0,
|
base: 0,
|
||||||
name: "Elation Boost",
|
name: transI18n("elation"),
|
||||||
icon: "spriteoutput/ui/avatar/icon/IconJoy.png",
|
icon: "spriteoutput/ui/avatar/icon/IconJoy.png",
|
||||||
unit: "%",
|
unit: "%",
|
||||||
round: 1
|
round: 1
|
||||||
@@ -284,7 +297,7 @@ export default function QuickView() {
|
|||||||
)
|
)
|
||||||
statsData.HP.base += calcBaseStatRaw(
|
statsData.HP.base += calcBaseStatRaw(
|
||||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
|
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
|
||||||
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHPAdd,
|
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd,
|
||||||
avatarProfile?.lightcone?.level
|
avatarProfile?.lightcone?.level
|
||||||
)
|
)
|
||||||
statsData.ATK.value += calcBaseStatRaw(
|
statsData.ATK.value += calcBaseStatRaw(
|
||||||
@@ -342,8 +355,6 @@ export default function QuickView() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (avatarProfile?.relics && mainAffix && subAffix) {
|
if (avatarProfile?.relics && mainAffix && subAffix) {
|
||||||
Object.entries(avatarProfile?.relics).forEach(([key, value]) => {
|
Object.entries(avatarProfile?.relics).forEach(([key, value]) => {
|
||||||
const mainAffixMap = mainAffix["5" + key]
|
const mainAffixMap = mainAffix["5" + key]
|
||||||
@@ -378,7 +389,6 @@ export default function QuickView() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return statsData
|
return statsData
|
||||||
}, [
|
}, [
|
||||||
avatarSelected,
|
avatarSelected,
|
||||||
@@ -395,64 +405,89 @@ export default function QuickView() {
|
|||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 pt-1">
|
||||||
<div className="col-span-1 md:col-span-2 flex flex-col justify-between py-3">
|
{/* Stats Overview Column - Clean Minimal Grid, No Box-in-Box */}
|
||||||
<div className="flex w-full flex-col justify-between gap-y-0.5 text-base">
|
<div className="lg:col-span-6 space-y-6">
|
||||||
{Object.entries(characterStats || {})?.map(([key, stat], index) => {
|
<div className="space-y-3">
|
||||||
if (!stat || (key.includes("Add") && stat.value === 0)) return null
|
<div className="flex items-center justify-between pb-2 border-b border-base-content/10">
|
||||||
return (
|
<div className="flex items-center gap-2">
|
||||||
<div key={index} className="flex flex-row items-center justify-between">
|
<span className="w-1 h-3.5 bg-primary rounded-full" />
|
||||||
<div className="flex flex-row items-center">
|
<h3 className="text-xs font-bold uppercase tracking-wider text-base-content/90">
|
||||||
<NextImage
|
{transI18n("character")}
|
||||||
src={`${process.env.CDN_URL}/${stat?.icon}`}
|
</h3>
|
||||||
unoptimized
|
</div>
|
||||||
crossOrigin="anonymous"
|
<span className="text-xs font-mono font-medium text-base-content/60">
|
||||||
alt="Stat Icon"
|
{transI18n("level")} {avatarData?.level || 1}
|
||||||
width={40}
|
</span>
|
||||||
height={40}
|
</div>
|
||||||
className="h-10 w-10 p-1 mx-1 bg-black/20 rounded-full"
|
|
||||||
/>
|
<div className="grid grid-cols-2 gap-x-6 gap-y-2 text-xs">
|
||||||
<div className="font-bold">{stat.name}</div>
|
{Object.entries(characterStats || {})?.map(([key, stat], index) => {
|
||||||
|
if (!stat || (key.includes("Add") && stat.value === 0)) return null
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="flex items-center justify-between py-1.5 border-b border-base-content/5"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 min-w-0 pr-2">
|
||||||
|
<NextImage
|
||||||
|
src={`${process.env.CDN_URL}/${stat?.icon}`}
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
alt={stat.name}
|
||||||
|
width={20}
|
||||||
|
height={20}
|
||||||
|
className="h-5 w-5 theme-stat-icon shrink-0 object-contain"
|
||||||
|
/>
|
||||||
|
<span className="font-medium text-base-content/80 truncate">{stat.name}</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-mono font-semibold text-primary shrink-0 text-right">
|
||||||
|
{stat.value ? stat.unit === "%" ? (stat.value * 100).toFixed(stat.round) : stat.value.toFixed(stat.round) : 0}
|
||||||
|
<span className="text-[10px] font-normal opacity-80 ml-0.5">{stat.unit}</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 mr-3 grow border rounded opacity-50" />
|
)
|
||||||
<div className="flex cursor-default flex-col text-right font-bold">{
|
})}
|
||||||
stat.value ? stat.unit === "%" ? (stat.value * 100).toFixed(stat.round) : stat.value.toFixed(stat.round) : 0
|
</div>
|
||||||
}{stat.unit}</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<hr />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-1 w-full my-2">
|
{/* Set Effects - Flat Minimalist Row */}
|
||||||
{relicEffects.map((setEffect, index) => {
|
{relicEffects.length > 0 && (
|
||||||
const relicInfo = mapRelicSet[setEffect.key];
|
<div className="space-y-2 pt-2">
|
||||||
if (!relicInfo) return null;
|
<div className="flex items-center gap-2 pb-1 border-b border-base-content/10">
|
||||||
return (
|
<span className="w-1 h-3.5 bg-amber-500 rounded-full" />
|
||||||
<div key={index} className="flex w-full flex-row justify-between text-left">
|
<h4 className="text-xs font-bold uppercase tracking-wider text-base-content/90">
|
||||||
<div
|
{transI18n("relics")}
|
||||||
className="font-bold truncate max-w-full mr-1"
|
</h4>
|
||||||
style={{
|
</div>
|
||||||
fontSize: 'clamp(0.5rem, 2vw, 1rem)'
|
<div className="space-y-1">
|
||||||
}}
|
{relicEffects.map((setEffect, index) => {
|
||||||
dangerouslySetInnerHTML={{
|
const relicInfo = mapRelicSet[setEffect.key];
|
||||||
__html: replaceByParam(
|
if (!relicInfo) return null;
|
||||||
getLocaleName(locale, relicInfo.Name),
|
return (
|
||||||
[]
|
<div
|
||||||
)
|
key={index}
|
||||||
}}
|
className="flex items-center justify-between text-xs py-1.5 border-b border-base-content/5"
|
||||||
/>
|
>
|
||||||
<div>
|
<div
|
||||||
<span className="black-blur bg-black/20 flex w-5 justify-center rounded px-1.5 py-0.5">{setEffect.count}</span>
|
className="font-medium text-base-content/90 truncate max-w-[80%]"
|
||||||
</div>
|
dangerouslySetInnerHTML={{
|
||||||
</div>
|
__html: replaceByParam(getLocaleName(locale, relicInfo.Name), [])
|
||||||
)
|
}}
|
||||||
})}
|
/>
|
||||||
</div>
|
<span className="font-mono font-semibold text-amber-600 dark:text-amber-400 text-xs shrink-0">
|
||||||
|
{setEffect.count} Pc
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-2 justify-between py-3 text-lg">
|
{/* Relic Equipment Cards Column */}
|
||||||
|
<div className="lg:col-span-6 space-y-2">
|
||||||
{relicStats?.map((relic, index) => {
|
{relicStats?.map((relic, index) => {
|
||||||
if (!relic || !avatarSelected) return null
|
if (!relic || !avatarSelected) return null
|
||||||
return (
|
return (
|
||||||
@@ -461,10 +496,8 @@ export default function QuickView() {
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{(!relicStats || !relicStats?.length) && (
|
{(!relicStats || !relicStats?.length) && (
|
||||||
<div className="flex flex-col items-center justify-center">
|
<div className="p-8 text-center text-base-content/40 text-xs italic">
|
||||||
<div className="text-center p-6 rounded-lg bg-black/40 backdrop-blur-sm border border-white/10">
|
{transI18n("noRelicEquipped")}
|
||||||
<span className="text-lg text-gray-400">{transI18n("noRelicEquipped")}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+252
-212
@@ -15,13 +15,37 @@ import useCurrentDataStore from '@/stores/currentDataStore';
|
|||||||
import { RelicSetDetail, SubAffixData } from '@/types';
|
import { RelicSetDetail, SubAffixData } from '@/types';
|
||||||
import useLocaleStore from '@/stores/localeStore';
|
import useLocaleStore from '@/stores/localeStore';
|
||||||
import { mappingRelicSlot } from "@/constant/constant";
|
import { mappingRelicSlot } from "@/constant/constant";
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
export default function RelicMaker() {
|
export default function RelicMaker() {
|
||||||
const { avatars, setAvatars } = useUserDataStore()
|
const { avatars, setAvatar } = useUserDataStore(
|
||||||
const { avatarSelected } = useCurrentDataStore()
|
useShallow((state) => ({
|
||||||
const { setIsOpenRelic } = useModelStore()
|
avatars: state.avatars,
|
||||||
const { mainAffix, subAffix, mapRelicSet } = useDetailDataStore()
|
setAvatar: state.setAvatar,
|
||||||
const { locale } = useLocaleStore()
|
}))
|
||||||
|
)
|
||||||
|
const { avatarSelected } = useCurrentDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
avatarSelected: state.avatarSelected,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { setIsOpenRelic } = useModelStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
setIsOpenRelic: state.setIsOpenRelic,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { mainAffix, subAffix, mapRelicSet } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
mainAffix: state.mainAffix,
|
||||||
|
subAffix: state.subAffix,
|
||||||
|
mapRelicSet: state.mapRelicSet,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { locale } = useLocaleStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
locale: state.locale,
|
||||||
|
}))
|
||||||
|
)
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
const {
|
const {
|
||||||
selectedRelicSlot,
|
selectedRelicSlot,
|
||||||
@@ -37,7 +61,23 @@ export default function RelicMaker() {
|
|||||||
resetHistory,
|
resetHistory,
|
||||||
popHistory,
|
popHistory,
|
||||||
addHistory,
|
addHistory,
|
||||||
} = useRelicMakerStore()
|
} = useRelicMakerStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
selectedRelicSlot: state.selectedRelicSlot,
|
||||||
|
selectedRelicSet: state.selectedRelicSet,
|
||||||
|
selectedMainStat: state.selectedMainStat,
|
||||||
|
listSelectedSubStats: state.listSelectedSubStats,
|
||||||
|
selectedRelicLevel: state.selectedRelicLevel,
|
||||||
|
preSelectedSubStats: state.preSelectedSubStats,
|
||||||
|
setSelectedRelicSet: state.setSelectedRelicSet,
|
||||||
|
setSelectedMainStat: state.setSelectedMainStat,
|
||||||
|
setSelectedRelicLevel: state.setSelectedRelicLevel,
|
||||||
|
setListSelectedSubStats: state.setListSelectedSubStats,
|
||||||
|
resetHistory: state.resetHistory,
|
||||||
|
popHistory: state.popHistory,
|
||||||
|
addHistory: state.addHistory,
|
||||||
|
}))
|
||||||
|
)
|
||||||
const [error, setError] = useState<string>("");
|
const [error, setError] = useState<string>("");
|
||||||
|
|
||||||
const relicSets = useMemo(() => {
|
const relicSets = useMemo(() => {
|
||||||
@@ -218,46 +258,57 @@ export default function RelicMaker() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
avatar.profileList[avatar.profileSelect].relics[selectedRelicSlot] = {
|
const profileIndex = avatar.profileSelect;
|
||||||
level: selectedRelicLevel,
|
const currentProfile = avatar.profileList[profileIndex];
|
||||||
relic_id: Number(`6${selectedRelicSet}${selectedRelicSlot}`),
|
if (currentProfile) {
|
||||||
relic_set_id: Number(selectedRelicSet),
|
const newProfileList = [...avatar.profileList];
|
||||||
main_affix_id: Number(selectedMainStat),
|
newProfileList[profileIndex] = {
|
||||||
sub_affixes: listSelectedSubStats.map((item) => {
|
...currentProfile,
|
||||||
return {
|
relics: {
|
||||||
sub_affix_id: Number(item.affixId),
|
...currentProfile.relics,
|
||||||
count: item.rollCount,
|
[selectedRelicSlot]: {
|
||||||
step: item.stepCount
|
level: selectedRelicLevel,
|
||||||
|
relic_id: Number(`6${selectedRelicSet}${selectedRelicSlot}`),
|
||||||
|
relic_set_id: Number(selectedRelicSet),
|
||||||
|
main_affix_id: Number(selectedMainStat),
|
||||||
|
sub_affixes: listSelectedSubStats.map((item) => ({
|
||||||
|
sub_affix_id: Number(item.affixId),
|
||||||
|
count: item.rollCount,
|
||||||
|
step: item.stepCount
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
};
|
||||||
|
setAvatar({
|
||||||
|
...avatar,
|
||||||
|
profileList: newProfileList
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setAvatars({ ...avatars });
|
|
||||||
setIsOpenRelic(false);
|
setIsOpenRelic(false);
|
||||||
|
|
||||||
toast.success(transI18n("relicSavedSuccessfully"));
|
toast.success(transI18n("relicSavedSuccessfully"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-5">
|
||||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
{/* Left Panel: Main Stat & Set Configuration */}
|
||||||
{transI18n("relicMaker")}
|
<div className="lg:col-span-5 space-y-4">
|
||||||
</h3>
|
<div className="p-4 sm:p-5 rounded-2xl bg-base-200/50 border border-base-content/10 shadow-xs space-y-4">
|
||||||
</div>
|
<div className="pb-3 border-b border-base-content/10">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-2">
|
<h2 className="text-base font-bold text-primary flex items-center gap-2">
|
||||||
|
<span className="w-1.5 h-4 bg-primary rounded-full"></span>
|
||||||
|
{transI18n("mainSettings")}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Left Panel */}
|
<div className="space-y-3.5">
|
||||||
<div className="space-y-6">
|
{/* Main Stat Picker */}
|
||||||
|
|
||||||
{/* Set Configuration */}
|
|
||||||
<div className="bg-base-100 rounded-xl p-6 border border-slate-700">
|
|
||||||
<h2 className="text-xl font-bold mb-6 text-warning">{transI18n("mainSettings")}</h2>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
|
||||||
{/* Main Stat */}
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-lg font-medium mb-2">{transI18n("mainStat")}</label>
|
<label className="block text-xs font-bold uppercase tracking-wider text-base-content/75 mb-1.5">
|
||||||
|
{transI18n("mainStat")}
|
||||||
|
</label>
|
||||||
<SelectCustomImage
|
<SelectCustomImage
|
||||||
customSet={Object.entries(mainAffix["5" + selectedRelicSlot] || {}).map(([key, value]) => ({
|
customSet={Object.entries(mainAffix["5" + selectedRelicSlot] || {}).map(([key, value]) => ({
|
||||||
value: key,
|
value: key,
|
||||||
@@ -270,9 +321,12 @@ export default function RelicMaker() {
|
|||||||
setSelectedCustomSet={setSelectedMainStat}
|
setSelectedCustomSet={setSelectedMainStat}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Relic Set Selection */}
|
|
||||||
|
{/* Relic Set Picker */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-lg font-medium mb-2">{transI18n("set")}</label>
|
<label className="block text-xs font-bold uppercase tracking-wider text-base-content/75 mb-1.5">
|
||||||
|
{transI18n("set")}
|
||||||
|
</label>
|
||||||
<SelectCustomImage
|
<SelectCustomImage
|
||||||
customSet={Object.entries(relicSets).map(([key, value]) => ({
|
customSet={Object.entries(relicSets).map(([key, value]) => ({
|
||||||
value: key,
|
value: key,
|
||||||
@@ -285,212 +339,198 @@ export default function RelicMaker() {
|
|||||||
setSelectedCustomSet={setSelectedRelicSet}
|
setSelectedCustomSet={setSelectedRelicSet}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Set Bonus Display */}
|
{/* Set Bonus Display */}
|
||||||
<div className="mb-6 py-4 bg-base-100 rounded-lg">
|
<div className="py-2 px-1 text-xs border-t border-b border-base-content/10">
|
||||||
{selectedRelicSet !== "" ? Object.entries(mapRelicSet[selectedRelicSet].Skills).map(([key, value]) => (
|
{selectedRelicSet !== "" && mapRelicSet[selectedRelicSet] ? (
|
||||||
<div key={key} className="text-blue-300 text-sm mb-1">
|
Object.entries(mapRelicSet[selectedRelicSet].Skills).map(([key, value]) => (
|
||||||
<span className="text-info font-bold">{key}-Pc:
|
<div key={key} className="mb-2 last:mb-0 space-y-0.5">
|
||||||
<div
|
<span className="font-bold text-amber-500">{key}-Piece:</span>
|
||||||
className="text-warning leading-relaxed font-bold"
|
<div
|
||||||
dangerouslySetInnerHTML={{
|
className="text-base-content/80 leading-relaxed pl-1"
|
||||||
__html: replaceByParam(
|
dangerouslySetInnerHTML={{
|
||||||
getLocaleName(locale, value.Desc),
|
__html: replaceByParam(
|
||||||
value.Param || []
|
getLocaleName(locale, value.Desc),
|
||||||
)
|
value.Param || []
|
||||||
}}
|
)
|
||||||
/> </span>
|
}}
|
||||||
</div>
|
/>
|
||||||
)) : <p className="text-blue-300 text-sm font-bold mb-1">{transI18n("pleaseSelectASet")}</p>}
|
</div>
|
||||||
</div>
|
))
|
||||||
|
) : (
|
||||||
|
<p className="text-base-content/40 italic py-1">{transI18n("pleaseSelectASet")}</p>
|
||||||
{/* Rarity */}
|
)}
|
||||||
<div className="grid grid-cols-2 items-center gap-4 mb-6">
|
|
||||||
<label className="block text-lg font-medium mb-2">{transI18n("rarity")}: {5} ⭐</label>
|
|
||||||
<label className="block text-lg font-medium mb-2">{transI18n("effectBonus")}: <span className="text-warning font-bold">{effectBonus}</span></label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Level */}
|
|
||||||
<div className="mb-6">
|
|
||||||
<label className="block text-lg font-medium mb-2">{transI18n("level")}</label>
|
|
||||||
<div className="bg-base-200 rounded-lg p-4">
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
min="0"
|
|
||||||
max="15"
|
|
||||||
value={selectedRelicLevel}
|
|
||||||
onChange={(e) => setSelectedRelicLevel(parseInt(e.target.value))}
|
|
||||||
className="range range-primary w-full"
|
|
||||||
/>
|
|
||||||
<div className="text-center text-2xl font-bold mt-2">{selectedRelicLevel}</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Effect Bonus & Level */}
|
||||||
|
<div className="space-y-3 pt-1">
|
||||||
|
<div className="flex items-center justify-between text-xs font-semibold">
|
||||||
|
<span className="text-base-content/75">{transI18n("effectBonus")}:</span>
|
||||||
|
<span className="font-mono text-primary font-bold text-sm">+{effectBonus}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex items-center justify-between text-xs font-semibold">
|
||||||
|
<span className="text-base-content/75">{transI18n("level")}</span>
|
||||||
|
<span className="font-mono text-primary font-bold">+{selectedRelicLevel}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="15"
|
||||||
|
value={selectedRelicLevel}
|
||||||
|
onChange={(e) => setSelectedRelicLevel(parseInt(e.target.value))}
|
||||||
|
className="range range-primary range-xs w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Error Message */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{error && (
|
||||||
|
<motion.p
|
||||||
|
key="error"
|
||||||
|
initial={{ opacity: 0, y: -4 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
className="text-xs text-error font-bold text-center bg-error/10 p-2 rounded-lg border border-error/20"
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</motion.p>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<button
|
||||||
|
onClick={handlerSaveRelic}
|
||||||
|
className="btn btn-primary w-full rounded-xl font-bold shadow-xs mt-2"
|
||||||
|
>
|
||||||
|
{transI18n("save")}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<AnimatePresence>
|
|
||||||
{error && (
|
|
||||||
<motion.p
|
|
||||||
key="error"
|
|
||||||
initial={{ x: 0 }}
|
|
||||||
animate={{ x: [0, -2, 2, -2, 2, 0] }}
|
|
||||||
transition={{
|
|
||||||
duration: 6,
|
|
||||||
repeat: Infinity,
|
|
||||||
ease: "easeInOut",
|
|
||||||
repeatDelay: 1.5,
|
|
||||||
}}
|
|
||||||
className="text-error my-2 text-center font-bold"
|
|
||||||
>
|
|
||||||
{error}!
|
|
||||||
</motion.p>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{/* Save Button */}
|
|
||||||
<button onClick={handlerSaveRelic} className="btn btn-success w-full">
|
|
||||||
{transI18n("save")}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Panel - Sub Stats */}
|
{/* Right Panel: Sub Stats Configurator */}
|
||||||
<div className="space-y-4">
|
<div className="lg:col-span-7 space-y-3">
|
||||||
{/* Total Roll */}
|
{/* Header with Total Rolls & Randomizers */}
|
||||||
<div className="bg-base-100 rounded-xl p-4 border border-slate-700 z-1">
|
<div className="p-3 rounded-2xl bg-base-200/50 border border-base-content/10 flex flex-wrap items-center justify-between gap-2 shadow-xs">
|
||||||
<h3 className="text-lg font-bold mb-4">{transI18n("totalRoll")} {listSelectedSubStats.reduce((a, b) => a + b.rollCount, 0)}</h3>
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs font-bold uppercase tracking-wider text-base-content/75">{transI18n("totalRoll")}:</span>
|
||||||
|
<span className="text-sm font-mono font-bold px-2 py-0.5 rounded-full bg-primary/20 text-primary border border-primary/30">
|
||||||
|
{listSelectedSubStats.reduce((a, b) => a + b.rollCount, 0)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline btn-success sm:btn-sm"
|
className="btn btn-xs btn-outline btn-primary rounded-lg font-semibold"
|
||||||
onClick={randomizeStats}
|
onClick={randomizeStats}
|
||||||
>
|
>
|
||||||
{transI18n("randomizeStats")}
|
{transI18n("randomizeStats")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="btn btn-outline btn-success sm:btn-sm"
|
className="btn btn-xs btn-outline btn-accent rounded-lg font-semibold"
|
||||||
onClick={randomizeRolls}
|
onClick={randomizeRolls}
|
||||||
>
|
>
|
||||||
{transI18n("randomizeRolls")}
|
{transI18n("randomizeRolls")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{listSelectedSubStats.map((v, index) => (
|
|
||||||
<div key={index} className={`bg-base-100 rounded-xl p-4 border border-slate-700`}>
|
|
||||||
<div className="grid grid-cols-12 gap-2 items-center">
|
|
||||||
|
|
||||||
{/* Stat Selection */}
|
{/* 4 Sub-Stat Slots */}
|
||||||
<div className="col-span-8">
|
<div className="space-y-2.5">
|
||||||
<SelectCustomImage
|
{listSelectedSubStats.map((v, index) => (
|
||||||
customSet={Object.entries(subAffixOptions).map(([key, value]) => ({
|
<div key={index} className="p-3.5 rounded-2xl bg-base-200/40 border border-base-content/10 space-y-2.5 shadow-xs">
|
||||||
value: key,
|
<div className="flex items-center justify-between gap-2">
|
||||||
label: mappingStats[value.Property].name + " " + mappingStats[value.Property].unit,
|
<div className="flex-1">
|
||||||
imageUrl: `${process.env.CDN_URL}/${mappingStats[value.Property].icon}`
|
<SelectCustomImage
|
||||||
}))}
|
customSet={Object.entries(subAffixOptions).map(([key, value]) => ({
|
||||||
excludeSet={Object.entries(exSubAffixOptions).map(([key, value]) => ({
|
value: key,
|
||||||
value: key,
|
label: mappingStats[value.Property].name + " " + mappingStats[value.Property].unit,
|
||||||
label: mappingStats[value.Property].name + " " + mappingStats[value.Property].unit,
|
imageUrl: `${process.env.CDN_URL}/${mappingStats[value.Property].icon}`
|
||||||
imageUrl: `${process.env.CDN_URL}/${mappingStats[value.Property].icon}`
|
}))}
|
||||||
}))}
|
excludeSet={Object.entries(exSubAffixOptions).map(([key, value]) => ({
|
||||||
selectedCustomSet={v.affixId}
|
value: key,
|
||||||
placeholder={transI18n("selectASubStat")}
|
label: mappingStats[value.Property].name + " " + mappingStats[value.Property].unit,
|
||||||
setSelectedCustomSet={(key) => handleSubStatChange(key, index, 0, 0)}
|
imageUrl: `${process.env.CDN_URL}/${mappingStats[value.Property].icon}`
|
||||||
/>
|
}))}
|
||||||
</div>
|
selectedCustomSet={v.affixId}
|
||||||
|
placeholder={transI18n("selectASubStat")}
|
||||||
{/* Current Value */}
|
setSelectedCustomSet={(key) => handleSubStatChange(key, index, 0, 0)}
|
||||||
<div className="col-span-4 text-center flex items-center justify-center gap-2">
|
/>
|
||||||
<span className="text-2xl font-mono">+{ }</span>
|
|
||||||
<div className="text-xl font-bold text-info">{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount, v.rollCount)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Up Roll Values */}
|
|
||||||
<div className="col-span-12">
|
|
||||||
<div className="flex items-center gap-2 mb-2">
|
|
||||||
<ChevronUp className="w-4 h-4 text-success" />
|
|
||||||
<span className="text-sm font-semibold text-success">{transI18n("upRoll")}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 gap-1">
|
|
||||||
<button
|
{/* Stat Value Readout */}
|
||||||
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 0)}
|
<div className="px-3 py-1 rounded-xl bg-base-100 border border-base-content/10 text-right min-w-24 shadow-xs">
|
||||||
className="btn btn-sm btn-info border-0"
|
<span className="text-sm font-mono font-extrabold text-primary block">
|
||||||
>
|
+{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount, v.rollCount)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
||||||
{calcAffixBonus(subAffixOptions[v.affixId], 0, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
</span>
|
||||||
</button>
|
<span className="text-[11px] text-base-content/60 font-mono font-semibold">
|
||||||
<button
|
Roll: {v.rollCount} | Step: {v.stepCount}
|
||||||
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 1)}
|
</span>
|
||||||
className="btn btn-sm btn-info border-0"
|
|
||||||
>
|
|
||||||
{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + 1, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 2)}
|
|
||||||
className="btn btn-sm btn-info border-0"
|
|
||||||
>
|
|
||||||
{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + 2, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Down Roll Values */}
|
{/* Up / Down Steppers */}
|
||||||
<div className="col-span-12">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 pt-0.5">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
{/* Up Rolls */}
|
||||||
<ChevronDown className="w-4 h-4 text-error" />
|
<div className="space-y-1">
|
||||||
<span className="text-sm font-semibold text-error">{transI18n("downRoll")}</span>
|
<div className="flex items-center gap-1 text-[11px] font-bold text-success">
|
||||||
</div>
|
<ChevronUp className="w-3.5 h-3.5" />
|
||||||
<div className="grid grid-cols-3 gap-1">
|
<span>{transI18n("upRoll")}</span>
|
||||||
<button
|
|
||||||
onClick={() => handleSubStatChange(v.affixId, index, Math.max(v.rollCount - 1, 0), Math.max(v.stepCount, 0))}
|
|
||||||
className="btn btn-sm btn-info border-0"
|
|
||||||
>
|
|
||||||
{calcAffixBonus(subAffixOptions[v.affixId], 0, Math.max(v.rollCount - 1, 0))}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleSubStatChange(v.affixId, index, Math.max(v.rollCount - 1, 0), Math.max(v.stepCount - 1, 0))}
|
|
||||||
className="btn btn-sm btn-info border-0"
|
|
||||||
>
|
|
||||||
{calcAffixBonus(subAffixOptions[v.affixId], Math.max(v.stepCount - 1, 0), Math.max(v.rollCount - 1, 0))}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleSubStatChange(v.affixId, index, Math.max(v.rollCount - 1, 0), Math.max(v.stepCount - 2, 0))}
|
|
||||||
className="btn btn-sm btn-info border-0"
|
|
||||||
>
|
|
||||||
{calcAffixBonus(subAffixOptions[v.affixId], Math.max(v.stepCount - 2, 0), Math.max(v.rollCount - 1, 0))}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Reset Button & Roll Info */}
|
|
||||||
<div className="col-span-12 text-center w-full">
|
|
||||||
<div className="grid grid-rows-2 gap-1 items-center justify-items-start w-full">
|
|
||||||
<div className="grid grid-cols-2 gap-2 items-center w-full">
|
|
||||||
<button
|
|
||||||
className="btn btn-error btn-sm mb-1"
|
|
||||||
onClick={() => resetSubStat(index)}
|
|
||||||
>
|
|
||||||
{transI18n("reset")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="btn btn-warning btn-sm mb-1"
|
|
||||||
onClick={() => handlerRollback(index)}
|
|
||||||
>
|
|
||||||
{transI18n("rollBack")}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-1.5">
|
||||||
|
{[0, 1, 2].map((stepOffset) => (
|
||||||
|
<button
|
||||||
|
key={stepOffset}
|
||||||
|
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + stepOffset)}
|
||||||
|
className="btn btn-xs btn-success rounded-xl font-mono font-bold shadow-xs"
|
||||||
|
>
|
||||||
|
+{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + stepOffset, v.rollCount + 1)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2 items-center w-full">
|
{/* Down Rolls */}
|
||||||
<span className="font-bold">{transI18n("roll")}: <span className="text-info">{v.rollCount}</span></span>
|
<div className="space-y-1">
|
||||||
<span className="font-bold">{transI18n("step")}: <span className="text-info">{v.stepCount}</span></span>
|
<div className="flex items-center gap-1 text-[11px] font-bold text-error">
|
||||||
|
<ChevronDown className="w-3.5 h-3.5" />
|
||||||
|
<span>{transI18n("downRoll")}</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-1.5">
|
||||||
|
{[0, 1, 2].map((stepOffset) => (
|
||||||
|
<button
|
||||||
|
key={stepOffset}
|
||||||
|
onClick={() => handleSubStatChange(v.affixId, index, Math.max(v.rollCount - 1, 0), Math.max(v.stepCount - stepOffset, 0))}
|
||||||
|
className="btn btn-xs btn-error rounded-xl font-mono font-bold shadow-xs"
|
||||||
|
>
|
||||||
|
-{calcAffixBonus(subAffixOptions[v.affixId], Math.max(v.stepCount - stepOffset, 0), Math.max(v.rollCount - 1, 0))}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Reset & Rollback actions */}
|
||||||
|
<div className="flex items-center justify-end gap-2 pt-1 border-t border-base-content/10">
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost btn-xs text-warning hover:bg-warning/10 rounded-lg text-xs font-bold"
|
||||||
|
onClick={() => handlerRollback(index)}
|
||||||
|
>
|
||||||
|
{transI18n("rollBack")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost btn-xs text-error hover:bg-error/10 rounded-lg text-xs font-bold"
|
||||||
|
onClick={() => resetSubStat(index)}
|
||||||
|
>
|
||||||
|
{transI18n("reset")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+212
-144
@@ -2,7 +2,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useCallback, useEffect, useMemo } from "react";
|
import { useCallback, useEffect, useMemo } from "react";
|
||||||
import RelicMaker from "../relicBar";
|
import RelicMaker from "../relicBar";
|
||||||
import { motion } from "framer-motion";
|
|
||||||
import useUserDataStore from "@/stores/userDataStore";
|
import useUserDataStore from "@/stores/userDataStore";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import RelicCard from "../card/relicCard";
|
import RelicCard from "../card/relicCard";
|
||||||
@@ -15,9 +14,15 @@ import useCurrentDataStore from "@/stores/currentDataStore";
|
|||||||
import useDetailDataStore from "@/stores/detailDataStore";
|
import useDetailDataStore from "@/stores/detailDataStore";
|
||||||
import { getLocaleName } from '@/helper';
|
import { getLocaleName } from '@/helper';
|
||||||
import useLocaleStore from "@/stores/localeStore";
|
import useLocaleStore from "@/stores/localeStore";
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
export default function RelicsInfo() {
|
export default function RelicsInfo() {
|
||||||
const { avatars, setAvatars } = useUserDataStore()
|
const { avatars, setAvatar } = useUserDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
avatars: state.avatars,
|
||||||
|
setAvatar: state.setAvatar,
|
||||||
|
}))
|
||||||
|
)
|
||||||
const {
|
const {
|
||||||
setSelectedRelicSlot,
|
setSelectedRelicSlot,
|
||||||
selectedRelicSlot,
|
selectedRelicSlot,
|
||||||
@@ -28,18 +33,50 @@ export default function RelicsInfo() {
|
|||||||
resetHistory,
|
resetHistory,
|
||||||
resetSubStat,
|
resetSubStat,
|
||||||
listSelectedSubStats,
|
listSelectedSubStats,
|
||||||
} = useRelicMakerStore()
|
} = useRelicMakerStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
setSelectedRelicSlot: state.setSelectedRelicSlot,
|
||||||
|
selectedRelicSlot: state.selectedRelicSlot,
|
||||||
|
setSelectedMainStat: state.setSelectedMainStat,
|
||||||
|
setSelectedRelicSet: state.setSelectedRelicSet,
|
||||||
|
setSelectedRelicLevel: state.setSelectedRelicLevel,
|
||||||
|
setListSelectedSubStats: state.setListSelectedSubStats,
|
||||||
|
resetHistory: state.resetHistory,
|
||||||
|
resetSubStat: state.resetSubStat,
|
||||||
|
listSelectedSubStats: state.listSelectedSubStats,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isOpenRelic,
|
isOpenRelic,
|
||||||
setIsOpenRelic,
|
setIsOpenRelic,
|
||||||
isOpenQuickView,
|
isOpenQuickView,
|
||||||
setIsOpenQuickView
|
setIsOpenQuickView
|
||||||
} = useModelStore()
|
} = useModelStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
isOpenRelic: state.isOpenRelic,
|
||||||
|
setIsOpenRelic: state.setIsOpenRelic,
|
||||||
|
isOpenQuickView: state.isOpenQuickView,
|
||||||
|
setIsOpenQuickView: state.setIsOpenQuickView,
|
||||||
|
}))
|
||||||
|
)
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
const { avatarSelected } = useCurrentDataStore()
|
const { avatarSelected } = useCurrentDataStore(
|
||||||
const { mapRelicSet, subAffix } = useDetailDataStore()
|
useShallow((state) => ({
|
||||||
const { locale } = useLocaleStore()
|
avatarSelected: state.avatarSelected,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { mapRelicSet, subAffix } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
mapRelicSet: state.mapRelicSet,
|
||||||
|
subAffix: state.subAffix,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { locale } = useLocaleStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
locale: state.locale,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
const handleShow = (modalId: string) => {
|
const handleShow = (modalId: string) => {
|
||||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||||
@@ -60,18 +97,33 @@ export default function RelicsInfo() {
|
|||||||
const getRelic = useCallback((slot: string) => {
|
const getRelic = useCallback((slot: string) => {
|
||||||
const avatar = avatars[avatarSelected?.ID.toString() || ""];
|
const avatar = avatars[avatarSelected?.ID.toString() || ""];
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
return avatar.profileList[avatar.profileSelect]?.relics[slot] || null;
|
const profile = avatar.profileList?.[avatar.profileSelect] ?? avatar.profileList?.[0];
|
||||||
|
return profile?.relics?.[slot] || null;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}, [avatars, avatarSelected]);
|
}, [avatars, avatarSelected]);
|
||||||
|
|
||||||
const handlerDeleteRelic = (slot: string) => {
|
const handlerDeleteRelic = (slot: string) => {
|
||||||
const avatar = avatars[avatarSelected?.ID.toString() || ""];
|
const avatarKey = avatarSelected?.ID.toString() || "";
|
||||||
|
const avatar = avatars[avatarKey];
|
||||||
if (avatar) {
|
if (avatar) {
|
||||||
delete avatar.profileList[avatar.profileSelect].relics[slot]
|
const profileIndex = avatar.profileSelect;
|
||||||
setAvatars({ ...avatars });
|
const currentProfile = avatar.profileList?.[profileIndex];
|
||||||
|
if (currentProfile) {
|
||||||
|
const newRelics = { ...currentProfile.relics };
|
||||||
|
delete newRelics[slot];
|
||||||
|
const newProfileList = [...avatar.profileList];
|
||||||
|
newProfileList[profileIndex] = {
|
||||||
|
...currentProfile,
|
||||||
|
relics: newRelics,
|
||||||
|
};
|
||||||
|
setAvatar({
|
||||||
|
...avatar,
|
||||||
|
profileList: newProfileList,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const handlerChangeRelic = (slot: string) => {
|
const handlerChangeRelic = (slot: string) => {
|
||||||
const relic = getRelic(slot)
|
const relic = getRelic(slot)
|
||||||
@@ -84,10 +136,12 @@ export default function RelicsInfo() {
|
|||||||
setSelectedRelicLevel(relic.level)
|
setSelectedRelicLevel(relic.level)
|
||||||
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
|
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
|
||||||
relic.sub_affixes.forEach((item, index) => {
|
relic.sub_affixes.forEach((item, index) => {
|
||||||
newSubAffixes[index].affixId = item.sub_affix_id.toString();
|
if (newSubAffixes[index]) {
|
||||||
newSubAffixes[index].property = subAffix["5"][item.sub_affix_id.toString()]?.Property || "";
|
newSubAffixes[index].affixId = item.sub_affix_id.toString();
|
||||||
newSubAffixes[index].rollCount = item.count || 0;
|
newSubAffixes[index].property = subAffix["5"]?.[item.sub_affix_id.toString()]?.Property || "";
|
||||||
newSubAffixes[index].stepCount = item.step || 0;
|
newSubAffixes[index].rollCount = item.count || 0;
|
||||||
|
newSubAffixes[index].stepCount = item.step || 0;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
setListSelectedSubStats(newSubAffixes)
|
setListSelectedSubStats(newSubAffixes)
|
||||||
} else {
|
} else {
|
||||||
@@ -111,12 +165,11 @@ export default function RelicsInfo() {
|
|||||||
const relicEffects = useMemo(() => {
|
const relicEffects = useMemo(() => {
|
||||||
const avatar = avatars[avatarSelected?.ID.toString() || ""];
|
const avatar = avatars[avatarSelected?.ID.toString() || ""];
|
||||||
const relicCount: { [key: string]: number } = {};
|
const relicCount: { [key: string]: number } = {};
|
||||||
if (avatar) {
|
const currentProfile = avatar?.profileList?.[avatar?.profileSelect] ?? avatar?.profileList?.[0];
|
||||||
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
|
if (currentProfile?.relics) {
|
||||||
if (relicCount[relic.relic_set_id]) {
|
for (const relic of Object.values(currentProfile.relics)) {
|
||||||
relicCount[relic.relic_set_id]++;
|
if (relic) {
|
||||||
} else {
|
relicCount[relic.relic_set_id] = (relicCount[relic.relic_set_id] || 0) + 1;
|
||||||
relicCount[relic.relic_set_id] = 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,7 +185,7 @@ export default function RelicsInfo() {
|
|||||||
const modalConfigs: ModalConfig[] = [
|
const modalConfigs: ModalConfig[] = [
|
||||||
{
|
{
|
||||||
id: "action_detail_modal",
|
id: "action_detail_modal",
|
||||||
title: "",
|
title: transI18n("changeRelic") || "Cài đặt di vật",
|
||||||
isOpen: isOpenRelic,
|
isOpen: isOpenRelic,
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
setIsOpenRelic(false)
|
setIsOpenRelic(false)
|
||||||
@@ -172,48 +225,66 @@ export default function RelicsInfo() {
|
|||||||
}, [isOpenRelic]);
|
}, [isOpenRelic]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
|
<div className="w-full space-y-4">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6">
|
||||||
|
{/* Left Section - Relic Equipment Grid */}
|
||||||
|
<div className="lg:col-span-2 space-y-4">
|
||||||
|
<div className="glass-card rounded-2xl p-4 sm:p-6 border border-base-content/10 shadow-xs space-y-6">
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b border-base-content/10">
|
||||||
|
<h2 className="text-lg sm:text-xl font-bold flex items-center gap-2.5">
|
||||||
|
<div className="w-1.5 h-5 bg-linear-to-b from-primary to-accent rounded-full"></div>
|
||||||
|
<span className="bg-clip-text text-transparent bg-linear-to-r from-primary to-accent font-bold tracking-tight">
|
||||||
|
{transI18n("relics")}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
|
||||||
{/* Left Section - Items Grid */}
|
<button
|
||||||
<div className="lg:col-span-2">
|
onClick={() => {
|
||||||
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
|
setIsOpenQuickView(true)
|
||||||
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
|
handleShow("quick_view_modal")
|
||||||
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
|
}}
|
||||||
{transI18n("relics")}
|
className="btn btn-sm btn-outline btn-primary rounded-xl font-bold shadow-xs"
|
||||||
</h2>
|
>
|
||||||
|
{transI18n("quickView")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
|
{/* 6-Slot Grid completely free of box-in-box */}
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-6 py-4">
|
||||||
{["1", "2", "3", "4", "5", "6"].map((item, index) => (
|
{["1", "2", "3", "4", "5", "6"].map((item, index) => (
|
||||||
<div key={index} className="relative group">
|
<div
|
||||||
<div
|
key={index}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (item === selectedRelicSlot) {
|
if (item === selectedRelicSlot) {
|
||||||
setSelectedRelicSlot("")
|
setSelectedRelicSlot("")
|
||||||
} else {
|
} else {
|
||||||
setSelectedRelicSlot(item)
|
setSelectedRelicSlot(item)
|
||||||
}
|
}
|
||||||
handlerChangeRelic(item)
|
handlerChangeRelic(item)
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer"
|
className={`relative group flex flex-col items-center justify-center p-2 rounded-2xl transition-all duration-200 cursor-pointer ${
|
||||||
>
|
item === selectedRelicSlot
|
||||||
<RelicCard
|
? "bg-primary/5 rounded-2xl ring-1.5 ring-primary/40"
|
||||||
slot={item}
|
: "hover:bg-base-content/5"
|
||||||
avatarId={avatarSelected?.ID.toString() || ""}
|
}`}
|
||||||
/>
|
>
|
||||||
</div>
|
<RelicCard
|
||||||
|
slot={item}
|
||||||
|
avatarId={avatarSelected?.ID.toString() || ""}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
|
{/* Micro Actions */}
|
||||||
|
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
handlerChangeRelic(item)
|
handlerChangeRelic(item)
|
||||||
}}
|
}}
|
||||||
className="btn btn-info p-1.5 rounded-full shadow-lg transition-colors duration-200"
|
className="btn btn-xs btn-circle btn-primary shadow-xs"
|
||||||
title={transI18n("changeRelic")}
|
title={transI18n("changeRelic")}
|
||||||
>
|
>
|
||||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536M4 20h4l10.293-10.293a1 1 0 000-1.414l-2.586-2.586a1 1 0 00-1.414 0L4 16v4z" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -225,11 +296,11 @@ export default function RelicsInfo() {
|
|||||||
handlerDeleteRelic(item)
|
handlerDeleteRelic(item)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="btn btn-error p-1.5 rounded-full shadow-lg transition-colors duration-200"
|
className="btn btn-xs btn-circle btn-error shadow-xs"
|
||||||
title={transI18n("deleteRelic")}
|
title={transI18n("deleteRelic")}
|
||||||
>
|
>
|
||||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M9 7V4h6v3m5 0H4" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -237,13 +308,13 @@ export default function RelicsInfo() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-2 mt-10">
|
|
||||||
|
{/* Bottom Actions Bar */}
|
||||||
|
<div className="grid grid-cols-2 gap-3 pt-2">
|
||||||
<button
|
<button
|
||||||
disabled={!selectedRelicSlot}
|
disabled={!selectedRelicSlot}
|
||||||
onClick={() => {
|
onClick={() => handlerChangeRelic(selectedRelicSlot)}
|
||||||
handlerChangeRelic(selectedRelicSlot)
|
className="btn btn-primary btn-sm rounded-xl font-bold shadow-xs"
|
||||||
}}
|
|
||||||
className="btn btn-info"
|
|
||||||
>
|
>
|
||||||
{transI18n("changeRelic")}
|
{transI18n("changeRelic")}
|
||||||
</button>
|
</button>
|
||||||
@@ -255,112 +326,109 @@ export default function RelicsInfo() {
|
|||||||
handlerDeleteRelic(selectedRelicSlot)
|
handlerDeleteRelic(selectedRelicSlot)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="btn btn-error"
|
className="btn btn-ghost btn-sm text-error hover:bg-error/20 rounded-xl"
|
||||||
>
|
>
|
||||||
{transI18n("deleteRelic")}
|
{transI18n("deleteRelic")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setIsOpenQuickView(true)
|
|
||||||
handleShow("quick_view_modal")
|
|
||||||
}}
|
|
||||||
className="btn btn-info w-full mt-2"
|
|
||||||
>
|
|
||||||
{transI18n("quickView")}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Section - Stats and Set Effects */}
|
{/* Right Section - Set Effects Showcase */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-4">
|
||||||
|
<div className="glass-card rounded-2xl p-4 sm:p-6 border border-base-content/10 shadow-xs space-y-4">
|
||||||
{/* Set Effects Panel */}
|
<div className="pb-3 border-b border-base-content/10">
|
||||||
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
|
<h3 className="text-lg sm:text-xl font-bold flex items-center gap-2.5">
|
||||||
<h3 className="flex items-center gap-2 text-xl font-bold mb-4 text-base-content">
|
<div className="w-1.5 h-5 bg-linear-to-b from-amber-400 to-amber-600 rounded-full"></div>
|
||||||
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
|
<span className="bg-clip-text text-transparent bg-linear-to-r from-amber-400 to-amber-600 font-bold tracking-tight">
|
||||||
{transI18n("setEffects")}
|
{transI18n("setEffects")}
|
||||||
</h3>
|
</span>
|
||||||
|
</h3>
|
||||||
<div className="space-y-6">
|
|
||||||
{relicEffects.map((setEffect, index) => {
|
|
||||||
const relicInfo = mapRelicSet[setEffect.key];
|
|
||||||
if (!relicInfo) return null;
|
|
||||||
return (
|
|
||||||
<div key={index} className="space-y-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div
|
|
||||||
className="font-bold text-warning"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: replaceByParam(
|
|
||||||
getLocaleName(locale, relicInfo.Name),
|
|
||||||
[]
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{setEffect.count && (
|
|
||||||
<span className={`text-sm text-info`}>
|
|
||||||
({setEffect.count})
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2 pl-4">
|
|
||||||
{Object.entries(relicInfo.Skills).map(([requireNum, value]) => {
|
|
||||||
if (Number(requireNum) > Number(setEffect.count)) return null;
|
|
||||||
return (
|
|
||||||
<div key={requireNum} className="space-y-1">
|
|
||||||
<div className={`font-medium text-success`}>
|
|
||||||
{requireNum}-PC:
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="text-sm text-base-content/80 leading-relaxed pl-4"
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: replaceByParam(
|
|
||||||
getLocaleName(locale, value.Desc),
|
|
||||||
value.Param || []
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{relicEffects.length > 0 ? (
|
||||||
|
<div className="divide-y divide-base-content/10">
|
||||||
|
{relicEffects.map((setEffect, index) => {
|
||||||
|
const relicInfo = mapRelicSet[setEffect.key];
|
||||||
|
if (!relicInfo) return null;
|
||||||
|
return (
|
||||||
|
<div key={index} className="py-3.5 first:pt-0 last:pb-0 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
className="font-bold text-sm text-amber-500"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: replaceByParam(
|
||||||
|
getLocaleName(locale, relicInfo.Name),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{setEffect.count && (
|
||||||
|
<span className="badge badge-primary badge-sm font-mono font-bold">
|
||||||
|
{setEffect.count} Pc
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 pt-1">
|
||||||
|
{Object.entries(relicInfo.Skills).map(([requireNum, value]) => {
|
||||||
|
if (Number(requireNum) > Number(setEffect.count)) return null;
|
||||||
|
return (
|
||||||
|
<div key={requireNum} className="text-xs space-y-0.5 pl-2 border-l-2 border-primary/40">
|
||||||
|
<div className="font-bold text-emerald-500">
|
||||||
|
{requireNum}-PC Bonus
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="text-base-content/80 leading-relaxed"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: replaceByParam(
|
||||||
|
getLocaleName(locale, value.Desc),
|
||||||
|
value.Param || []
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-10 text-base-content/40 text-xs italic">
|
||||||
|
{transI18n("noRelicEquipped")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Modals */}
|
||||||
{modalConfigs.map(({ id, title, onClose, content }) => (
|
{modalConfigs.map(({ id, title, onClose, content }) => (
|
||||||
<dialog key={id} id={id} className="modal">
|
<dialog key={id} id={id} className="modal">
|
||||||
<div className="modal-box w-11/12 max-w-[90%] max-h-[85vh] bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
|
<div className={`modal-box ${id === 'quick_view_modal' ? 'w-11/12 max-w-4xl' : 'w-11/12 max-w-[92vw] sm:max-w-[88vw]'} max-h-[90vh] bg-base-100 text-base-content border border-base-content/10 shadow-2xl rounded-2xl p-0 overflow-hidden`}>
|
||||||
<div className="sticky top-0 z-10">
|
<div className="sticky top-0 z-20 flex items-center justify-between px-6 py-4 border-b border-base-content/10 bg-base-100">
|
||||||
<motion.button
|
<h3 className="font-bold text-xl tracking-tight bg-clip-text text-transparent bg-linear-to-r from-primary via-accent to-secondary">
|
||||||
whileHover={{ scale: 1.1, rotate: 90 }}
|
{title}
|
||||||
transition={{ duration: 0.2 }}
|
</h3>
|
||||||
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
|
<button
|
||||||
|
className="btn btn-sm btn-circle btn-ghost text-base-content/60 hover:text-base-content hover:bg-base-200 transition-colors"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</motion.button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{title && (
|
<div className="p-4 sm:p-6 overflow-y-auto max-h-[calc(90vh-5rem)]">
|
||||||
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
|
{content}
|
||||||
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
|
</div>
|
||||||
{title}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{content}
|
|
||||||
</div>
|
</div>
|
||||||
|
<form method="dialog" className="modal-backdrop">
|
||||||
|
<button onClick={onClose}>close</button>
|
||||||
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import dynamic from "next/dynamic"
|
|||||||
import type { SingleValue } from "react-select"
|
import type { SingleValue } from "react-select"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import useLocaleStore from "@/stores/localeStore"
|
import useLocaleStore from "@/stores/localeStore"
|
||||||
|
import { useShallow } from "zustand/react/shallow"
|
||||||
import ParseText from "../parseText"
|
import ParseText from "../parseText"
|
||||||
import { themeColors } from "@/constant/constant"
|
import { themeColors } from "@/constant/constant"
|
||||||
import type { Props as SelectProps } from "react-select"
|
import type { Props as SelectProps } from "react-select"
|
||||||
@@ -32,7 +33,12 @@ type SelectCustomProp = {
|
|||||||
|
|
||||||
export default function SelectCustomImage({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
|
export default function SelectCustomImage({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
|
||||||
const options: SelectOption[] = customSet
|
const options: SelectOption[] = customSet
|
||||||
const { locale, theme } = useLocaleStore()
|
const { locale, theme } = useLocaleStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
locale: state.locale,
|
||||||
|
theme: state.theme,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
const c = themeColors[theme] || themeColors.winter
|
const c = themeColors[theme] || themeColors.winter
|
||||||
|
|
||||||
@@ -42,10 +48,11 @@ export default function SelectCustomImage({ customSet, excludeSet, selectedCusto
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
padding: '8px 12px',
|
padding: '10px 14px',
|
||||||
backgroundColor: s.isFocused ? c.bgHover : c.bg,
|
backgroundColor: s.isFocused ? c.bgHover : c.bg,
|
||||||
color: c.text,
|
color: c.text,
|
||||||
cursor: 'pointer'
|
cursor: 'pointer',
|
||||||
|
opacity: 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
singleValue: (p: any) => ({
|
singleValue: (p: any) => ({
|
||||||
@@ -53,26 +60,43 @@ export default function SelectCustomImage({ customSet, excludeSet, selectedCusto
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '8px',
|
gap: '8px',
|
||||||
color: c.text
|
color: c.text,
|
||||||
|
opacity: 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
control: (p: any) => ({
|
control: (p: any) => ({
|
||||||
...p,
|
...p,
|
||||||
backgroundColor: c.bg,
|
backgroundColor: c.bg,
|
||||||
borderColor: c.border,
|
borderColor: c.border,
|
||||||
boxShadow: 'none'
|
boxShadow: 'none',
|
||||||
|
opacity: 1,
|
||||||
|
minHeight: '38px',
|
||||||
|
borderRadius: '12px',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
menu: (p: any) => ({
|
menu: (p: any) => ({
|
||||||
...p,
|
...p,
|
||||||
backgroundColor: c.bg,
|
backgroundColor: c.bg,
|
||||||
color: c.text,
|
color: c.text,
|
||||||
zIndex: 9999
|
zIndex: 99999,
|
||||||
|
opacity: 1,
|
||||||
|
border: `1px solid ${c.border}`,
|
||||||
|
boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.3), 0 8px 10px -6px rgba(0, 0, 0, 0.3)',
|
||||||
|
borderRadius: '12px',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}),
|
||||||
|
|
||||||
|
menuList: (p: any) => ({
|
||||||
|
...p,
|
||||||
|
backgroundColor: c.bg,
|
||||||
|
padding: 0,
|
||||||
|
opacity: 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
menuPortal: (p: any) => ({
|
menuPortal: (p: any) => ({
|
||||||
...p,
|
...p,
|
||||||
zIndex: 9999
|
zIndex: 99999,
|
||||||
|
opacity: 1,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import useLocaleStore from '@/stores/localeStore'
|
|||||||
import { themeColors } from '@/constant/constant'
|
import { themeColors } from '@/constant/constant'
|
||||||
import type { Props as SelectProps } from "react-select"
|
import type { Props as SelectProps } from "react-select"
|
||||||
import { JSX } from "react"
|
import { JSX } from "react"
|
||||||
|
import { useShallow } from "zustand/react/shallow"
|
||||||
|
|
||||||
const Select = dynamic(
|
const Select = dynamic(
|
||||||
() => import("react-select").then(m => m.default),
|
() => import("react-select").then(m => m.default),
|
||||||
@@ -32,7 +33,9 @@ type SelectCustomProp = {
|
|||||||
|
|
||||||
export default function SelectCustomText({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
|
export default function SelectCustomText({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
|
||||||
const options: SelectOption[] = customSet
|
const options: SelectOption[] = customSet
|
||||||
const { theme } = useLocaleStore()
|
const { theme } = useLocaleStore(
|
||||||
|
useShallow((state) => ({ theme: state.theme }))
|
||||||
|
)
|
||||||
const c = themeColors[theme] || themeColors.winter
|
const c = themeColors[theme] || themeColors.winter
|
||||||
|
|
||||||
const customStyles = {
|
const customStyles = {
|
||||||
@@ -87,7 +90,9 @@ export default function SelectCustomText({ customSet, excludeSet, selectedCustom
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{option.time && <div className='text-base'>{option.time}</div>}
|
{option.time && !option.time.includes("undefined") && (
|
||||||
|
<div className='text-sm opacity-70'>{option.time}</div>
|
||||||
|
)}
|
||||||
{option.description && <div
|
{option.description && <div
|
||||||
className="text-base"
|
className="text-base"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
|
|||||||
@@ -14,16 +14,33 @@ import RelicShowcase from './relicShowcase';
|
|||||||
import useDetailDataStore from '@/stores/detailDataStore';
|
import useDetailDataStore from '@/stores/detailDataStore';
|
||||||
import useCurrentDataStore from '@/stores/currentDataStore';
|
import useCurrentDataStore from '@/stores/currentDataStore';
|
||||||
import { getLocaleName } from '@/helper';
|
import { getLocaleName } from '@/helper';
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
|
|
||||||
export default function ShowCaseInfo() {
|
export default function ShowCaseInfo() {
|
||||||
const { avatarSelected } = useCurrentDataStore()
|
const { avatarSelected } = useCurrentDataStore(
|
||||||
const { mainAffix, subAffix, mapRelicSet, mapAvatar, mapLightCone, baseType, damageType } = useDetailDataStore()
|
useShallow((state) => ({ avatarSelected: state.avatarSelected }))
|
||||||
const { avatars } = useUserDataStore()
|
)
|
||||||
|
const { mainAffix, subAffix, mapRelicSet, mapAvatar, mapLightCone, baseType, damageType } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
mainAffix: state.mainAffix,
|
||||||
|
subAffix: state.subAffix,
|
||||||
|
mapRelicSet: state.mapRelicSet,
|
||||||
|
mapAvatar: state.mapAvatar,
|
||||||
|
mapLightCone: state.mapLightCone,
|
||||||
|
baseType: state.baseType,
|
||||||
|
damageType: state.damageType,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { avatars } = useUserDataStore(
|
||||||
|
useShallow((state) => ({ avatars: state.avatars }))
|
||||||
|
)
|
||||||
const [avgColor, setAvgColor] = useState('#222');
|
const [avgColor, setAvgColor] = useState('#222');
|
||||||
const imgRef = useRef(null);
|
const imgRef = useRef(null);
|
||||||
const cardRef = useRef(null)
|
const cardRef = useRef(null)
|
||||||
const { locale } = useLocaleStore()
|
const { locale } = useLocaleStore(
|
||||||
|
useShallow((state) => ({ locale: state.locale }))
|
||||||
|
)
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
|
|
||||||
const handleSaveImage = useCallback(() => {
|
const handleSaveImage = useCallback(() => {
|
||||||
@@ -88,11 +105,11 @@ export default function ShowCaseInfo() {
|
|||||||
|
|
||||||
const avatarProfile = useMemo(() => {
|
const avatarProfile = useMemo(() => {
|
||||||
if (!avatarSelected || !avatarData) return
|
if (!avatarSelected || !avatarData) return
|
||||||
return avatarData?.profileList?.[avatarData?.profileSelect]
|
return avatarData?.profileList?.[avatarData?.profileSelect] ?? avatarData?.profileList?.[0]
|
||||||
}, [avatarSelected, avatarData])
|
}, [avatarSelected, avatarData])
|
||||||
|
|
||||||
const lightconeStats = useMemo(() => {
|
const lightconeStats = useMemo(() => {
|
||||||
if (!avatarSelected || !avatarProfile?.lightcone || !mapLightCone[avatarProfile?.lightcone?.item_id]) return
|
if (!avatarSelected || !avatarProfile?.lightcone || !mapLightCone?.[avatarProfile?.lightcone?.item_id]) return
|
||||||
const promotion = calcPromotion(avatarProfile?.lightcone?.level)
|
const promotion = calcPromotion(avatarProfile?.lightcone?.level)
|
||||||
const atkStat = calcBaseStat(
|
const atkStat = calcBaseStat(
|
||||||
mapLightCone[avatarProfile?.lightcone?.item_id]?.Stats?.[promotion]?.BaseAttack,
|
mapLightCone[avatarProfile?.lightcone?.item_id]?.Stats?.[promotion]?.BaseAttack,
|
||||||
@@ -122,12 +139,11 @@ export default function ShowCaseInfo() {
|
|||||||
const relicEffects = useMemo(() => {
|
const relicEffects = useMemo(() => {
|
||||||
const avatar = avatars[avatarSelected?.ID?.toString() || ""];
|
const avatar = avatars[avatarSelected?.ID?.toString() || ""];
|
||||||
const relicCount: { [key: string]: number } = {};
|
const relicCount: { [key: string]: number } = {};
|
||||||
if (avatar) {
|
const profile = avatar?.profileList?.[avatar?.profileSelect] ?? avatar?.profileList?.[0];
|
||||||
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
|
if (profile?.relics) {
|
||||||
if (relicCount[relic.relic_set_id]) {
|
for (const relic of Object.values(profile.relics)) {
|
||||||
relicCount[relic.relic_set_id]++;
|
if (relic && relic.relic_set_id) {
|
||||||
} else {
|
relicCount[relic.relic_set_id] = (relicCount[relic.relic_set_id] || 0) + 1;
|
||||||
relicCount[relic.relic_set_id] = 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -497,21 +513,34 @@ export default function ShowCaseInfo() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-start m-1 text-white">
|
<div className="w-full space-y-4">
|
||||||
<div className="flex items-center justify-start mt-4 mb-4">
|
<div className="bg-base-100 rounded-2xl p-4 sm:p-6 border border-base-300 shadow-sm space-y-6">
|
||||||
<button className="btn btn-success w-24 text-sm" onClick={handleSaveImage}>Save Img</button>
|
{/* Header */}
|
||||||
</div>
|
<div className="flex items-center justify-between pb-3 border-b border-base-content/10 flex-wrap gap-3">
|
||||||
|
<h2 className="text-xl sm:text-2xl font-extrabold flex items-center gap-2.5">
|
||||||
|
<div className="w-2 h-6 bg-linear-to-b from-primary to-accent rounded-full"></div>
|
||||||
|
<span className="bg-clip-text text-transparent bg-linear-to-r from-primary to-accent">
|
||||||
|
{transI18n("showcaseCard")}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-primary rounded-xl font-bold shadow-xs active-press"
|
||||||
|
onClick={handleSaveImage}
|
||||||
|
>
|
||||||
|
{transI18n("save")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="overflow-auto">
|
<div className="overflow-auto rounded-2xl">
|
||||||
<div
|
<div
|
||||||
ref={cardRef}
|
ref={cardRef}
|
||||||
className=" relative min-h-162.5 w-400 rounded-3xl transition-all duration-500 overflow-hidden"
|
className="relative min-h-162.5 w-400 rounded-3xl transition-all duration-500 overflow-hidden text-white"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: `${applyBrightness(avgColor, 0.3)}`,
|
backgroundColor: `${applyBrightness(avgColor, 0.3)}`,
|
||||||
backdropFilter: "blur(50px)",
|
backdropFilter: "blur(50px)",
|
||||||
WebkitBackdropFilter: "blur(50px)",
|
WebkitBackdropFilter: "blur(50px)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="absolute bottom-2 left-4 z-10">
|
<div className="absolute bottom-2 left-4 z-10">
|
||||||
<span className="shadow-black [text-shadow:1px_1px_2px_var(--tw-shadow-color)]"></span>
|
<span className="shadow-black [text-shadow:1px_1px_2px_var(--tw-shadow-color)]"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -630,9 +659,9 @@ export default function ShowCaseInfo() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center gap-4 mt-2">
|
<div className="flex flex-row items-center gap-4 mt-2">
|
||||||
<div className="text-2xl text-[#d8b46e]">Lv. <span className="text-white">{avatarData?.level}</span>/<span className="text-neutral-400">80</span></div>
|
<div className="text-2xl text-[#d8b46e]">Lv. <span className="text-white">{avatarData?.level}</span>/<span className="text-neutral-400">80</span></div>
|
||||||
<span className="px-1.5 py-0.5 rounded-full text-lg font-bold text-[#E6D5B5] bg-[#5c4022] border-2 border-[#d8b46e] shadow-[0_0_10px_rgba(250,204,21,0.3)] select-none">
|
<div className="flex items-center justify-center min-w-8 h-8 px-1.5 rounded-full text-base font-bold text-[#E6D5B5] bg-[#5c4022] border-2 border-[#d8b46e] shadow-[0_0_10px_rgba(250,204,21,0.3)] select-none">
|
||||||
{totalSubStats}
|
{totalSubStats}
|
||||||
</span>
|
</div>
|
||||||
|
|
||||||
{avatarSelected && (
|
{avatarSelected && (
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
@@ -921,12 +950,12 @@ export default function ShowCaseInfo() {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex h-162.5 w-1/3 flex-col justify-between py-3 z-10">
|
<div className="flex h-162.5 w-1/3 flex-col justify-between py-3 z-10 text-white">
|
||||||
<div className="flex w-full flex-col justify-between gap-y-0.5 text-base h-125">
|
<div className="flex w-full flex-col justify-between gap-y-0.5 text-base h-125 text-white">
|
||||||
{Object.entries(characterStats || {})?.map(([key, stat], index) => {
|
{Object.entries(characterStats || {})?.map(([key, stat], index) => {
|
||||||
if (!stat || (key.includes("Add") && stat.value === 0)) return null
|
if (!stat || (key.includes("Add") && stat.value === 0)) return null
|
||||||
return (
|
return (
|
||||||
<div key={index} className="flex flex-row items-center justify-between">
|
<div key={index} className="flex flex-row items-center justify-between text-white">
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<NextImage src={`${process.env.CDN_URL}/${stat?.icon}`}
|
<NextImage src={`${process.env.CDN_URL}/${stat?.icon}`}
|
||||||
unoptimized
|
unoptimized
|
||||||
@@ -936,16 +965,16 @@ export default function ShowCaseInfo() {
|
|||||||
height={40}
|
height={40}
|
||||||
className="h-10 w-10 p-2"
|
className="h-10 w-10 p-2"
|
||||||
/>
|
/>
|
||||||
<span className="font-bold">{stat.name}</span>
|
<span className="font-bold text-white">{stat.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-3 mr-3 grow border rounded opacity-50" />
|
<div className="ml-3 mr-3 grow border-b border-white/20" />
|
||||||
<div className="flex cursor-default flex-col text-right font-bold">{
|
<div className="flex cursor-default flex-col text-right font-bold text-white">{
|
||||||
stat.value ? stat.unit === "%" ? (stat.value * 100).toFixed(stat.round) : stat.value.toFixed(stat.round) : 0
|
stat.value ? stat.unit === "%" ? (stat.value * 100).toFixed(stat.round) : stat.value.toFixed(stat.round) : 0
|
||||||
}{stat.unit}</div>
|
}{stat.unit}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<hr />
|
<hr className="border-white/20" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-1 w-full my-2">
|
<div className="flex flex-col items-center gap-1 w-full my-2">
|
||||||
@@ -1000,7 +1029,7 @@ export default function ShowCaseInfo() {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
|
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import NextImage from "next/image"
|
import NextImage from "next/image"
|
||||||
import { AvatarDetail, RelicShowcaseType } from "@/types";
|
import { AvatarDetail, RelicShowcaseType } from "@/types";
|
||||||
|
|
||||||
@@ -12,97 +11,84 @@ export default function RelicShowcase({
|
|||||||
avatarInfo: AvatarDetail;
|
avatarInfo: AvatarDetail;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex items-center gap-3 py-2 px-1 border-b border-base-content/10 last:border-b-0">
|
||||||
<div
|
{/* Relic Figure Icon */}
|
||||||
className="relative w-full flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600/60 bg-linear-to-r from-yellow-600/20 to-transparent"
|
<div className="relative shrink-0 flex flex-col items-center">
|
||||||
>
|
<NextImage
|
||||||
{/* Subtle glow overlay */}
|
src={relic?.img || ""}
|
||||||
<div className="absolute inset-0 rounded-s-lg pointer-events-none"></div>
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
alt="Relic"
|
||||||
|
className="h-12 w-12 object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex relative">
|
{/* Main Affix - Clean Display */}
|
||||||
<div className="absolute inset-0 rounded-lg blur-lg -z-10"></div>
|
<div className="flex w-20 shrink-0 flex-col items-start justify-center">
|
||||||
<NextImage
|
<div className="flex items-center gap-1">
|
||||||
src={relic?.img || ""}
|
{relic?.mainAffix?.detail?.icon ? (
|
||||||
unoptimized
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
width={78}
|
|
||||||
height={78}
|
|
||||||
alt="Relic Icon"
|
|
||||||
className="h-19.5 w-19.5 rounded-lg"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="absolute text-yellow-400 font-bold z-10 drop-shadow-[0_0_6px_rgba(251,191,36,0.8)]"
|
|
||||||
style={{
|
|
||||||
left: '0.65rem',
|
|
||||||
bottom: '-0.45rem',
|
|
||||||
fontSize: '1.05rem',
|
|
||||||
letterSpacing: '-0.1em',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
✦✦✦✦✦
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className=" flex w-1/6 flex-col items-center justify-center">
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-0 bg-yellow-500/15 rounded-full blur-md -z-10"></div>
|
|
||||||
<NextImage
|
<NextImage
|
||||||
src={`${process.env.CDN_URL}/${relic?.mainAffix?.detail?.icon}` || ""}
|
src={`${process.env.CDN_URL}/${relic.mainAffix.detail.icon}`}
|
||||||
unoptimized
|
unoptimized
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
width={35}
|
width={16}
|
||||||
height={35}
|
height={16}
|
||||||
alt="Main Affix Icon"
|
alt="Main Affix"
|
||||||
className="h-8.75 w-8.75"
|
className="h-4 w-4 theme-stat-icon shrink-0 object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
) : null}
|
||||||
<span className="text-base text-yellow-400 font-semibold drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]">
|
<span className="text-xs font-bold text-amber-600 dark:text-amber-400 font-mono">
|
||||||
{relic?.mainAffix?.valueAffix + relic?.mainAffix?.detail?.unit}
|
{relic?.mainAffix?.valueAffix + (relic?.mainAffix?.detail?.unit || "")}
|
||||||
</span>
|
|
||||||
<span className="bg-black/20 backdrop-blur-sm rounded px-1.5 py-0.5 text-xs border border-white/10">
|
|
||||||
+{relic?.mainAffix?.level}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-[10px] font-mono text-base-content/50 ml-0.5">
|
||||||
<div style={{ opacity: 0.3, height: '78px', borderLeftWidth: '1px' }}></div>
|
+{relic?.mainAffix?.level}
|
||||||
|
</span>
|
||||||
<div className="grid w-[65%] m-2 grid-cols-2 gap-1">
|
|
||||||
{relic?.subAffix?.map((subAffix, index) => {
|
|
||||||
if (!subAffix) return null
|
|
||||||
return (
|
|
||||||
<div key={index} className="flex flex-col">
|
|
||||||
<div className="relative flex flex-row items-center bg-black/20 backdrop-blur-sm rounded-md p-1 border border-white/5 min-w-0">
|
|
||||||
{subAffix?.detail?.icon ? (
|
|
||||||
<NextImage
|
|
||||||
src={`${process.env.CDN_URL}/${subAffix?.detail?.icon}` || ""}
|
|
||||||
unoptimized
|
|
||||||
crossOrigin="anonymous"
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
alt="Sub Affix Icon"
|
|
||||||
className="h-6 w-6 shrink-0"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="h-6 w-6 bg-black/60 rounded flex items-center justify-center border border-white/10 shrink-0">
|
|
||||||
<span className="text-xs text-white/50">?</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<span className="text-xs text-gray-200 ml-0.5 truncate flex-1 min-w-0">
|
|
||||||
+{subAffix?.valueAffix + subAffix?.detail?.unit}
|
|
||||||
</span>
|
|
||||||
{
|
|
||||||
(avatarInfo?.Relics?.SubAffixPropertyList.findIndex((item) => item === subAffix?.property) !== -1) && (
|
|
||||||
<span className="ml-1 bg-yellow-600/20 text-yellow-400 rounded-full px-1 py-0.5 text-[10px] font-semibold border border-yellow-600/30 shrink-0 leading-none">
|
|
||||||
{subAffix?.count}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
|
{/* Sub Affixes Grid - Clean Flat Typography */}
|
||||||
|
<div className="grid flex-1 grid-cols-2 gap-x-3 gap-y-1">
|
||||||
|
{relic?.subAffix?.map((subAffix, index) => {
|
||||||
|
if (!subAffix) return null
|
||||||
|
const isOptimalStat = avatarInfo?.Relics?.SubAffixPropertyList?.includes(subAffix?.property)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`flex items-center justify-between py-0.5 min-w-0 ${
|
||||||
|
isOptimalStat
|
||||||
|
? "text-amber-600 dark:text-amber-400 font-semibold"
|
||||||
|
: "text-base-content/75"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-1 min-w-0 flex-1">
|
||||||
|
{subAffix?.detail?.icon ? (
|
||||||
|
<NextImage
|
||||||
|
src={`${process.env.CDN_URL}/${subAffix.detail.icon}`}
|
||||||
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
|
width={14}
|
||||||
|
height={14}
|
||||||
|
alt="Sub Affix"
|
||||||
|
className="h-3.5 w-3.5 theme-stat-icon shrink-0 object-contain"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
<span className="text-xs font-mono truncate">
|
||||||
|
+{subAffix?.valueAffix + (subAffix?.detail?.unit || "")}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isOptimalStat && subAffix?.count ? (
|
||||||
|
<span className="ml-1 text-[10px] font-bold text-amber-600/80 dark:text-amber-400/80 shrink-0 font-mono">
|
||||||
|
({subAffix?.count})
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+248
-230
@@ -12,13 +12,26 @@ import { toast } from "react-toastify";
|
|||||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||||
import { StatusAdd } from '@/types/avatarDetail';
|
import { StatusAdd } from '@/types/avatarDetail';
|
||||||
import { SkillDescription } from "./skillDescription";
|
import { SkillDescription } from "./skillDescription";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
|
||||||
export default function SkillsInfo() {
|
export default function SkillsInfo() {
|
||||||
const transI18n = useTranslations("DataPage")
|
const transI18n = useTranslations("DataPage")
|
||||||
const { theme } = useLocaleStore()
|
const { theme, locale } = useLocaleStore(
|
||||||
const { avatarSelected, skillIDSelected, setSkillIDSelected } = useCurrentDataStore()
|
useShallow((state) => ({ theme: state.theme, locale: state.locale }))
|
||||||
const { avatars, setAvatar } = useUserDataStore()
|
)
|
||||||
const { locale } = useLocaleStore()
|
const { avatarSelected, skillIDSelected, setSkillIDSelected } = useCurrentDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
avatarSelected: state.avatarSelected,
|
||||||
|
skillIDSelected: state.skillIDSelected,
|
||||||
|
setSkillIDSelected: state.setSkillIDSelected,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { avatars, setAvatar } = useUserDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
avatars: state.avatars,
|
||||||
|
setAvatar: state.setAvatar,
|
||||||
|
}))
|
||||||
|
)
|
||||||
const traceButtons = useMemo(() => {
|
const traceButtons = useMemo(() => {
|
||||||
if (!avatarSelected) return
|
if (!avatarSelected) return
|
||||||
return traceButtonsInfo[avatarSelected.BaseType]
|
return traceButtonsInfo[avatarSelected.BaseType]
|
||||||
@@ -116,268 +129,273 @@ export default function SkillsInfo() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 sm:gap-6">
|
||||||
<div className="rounded-xl p-6 shadow-lg">
|
{/* Left Panel: Skill Tree Visualizer */}
|
||||||
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
|
<div className="lg:col-span-6 space-y-4">
|
||||||
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
|
<div className="glass-card rounded-2xl p-4 sm:p-6 border border-base-300 shadow-sm space-y-4">
|
||||||
{transI18n("skills")}
|
<div className="flex items-center justify-between pb-3 border-b border-base-300">
|
||||||
</h2>
|
<h2 className="text-lg sm:text-xl font-bold flex items-center gap-2.5">
|
||||||
<div className="flex flex-col items-center">
|
<div className="w-1.5 h-5 bg-linear-to-b from-primary to-accent rounded-full"></div>
|
||||||
<button className="btn btn-success" onClick={handlerMaxAll}>{transI18n("maxAll")}</button>
|
<span className="bg-clip-text text-transparent bg-linear-to-r from-primary to-accent font-bold tracking-tight">
|
||||||
{traceButtons && avatarSelected && (
|
{transI18n("skills")}
|
||||||
<div className="grid col-span-4 relative w-full aspect-square">
|
</span>
|
||||||
<Image
|
</h2>
|
||||||
unoptimized
|
<button
|
||||||
crossOrigin="anonymous"
|
className="btn btn-sm btn-primary rounded-xl font-bold shadow-xs active-press"
|
||||||
src={`/skilltree/${avatarSelected?.BaseType?.toUpperCase()}.webp`}
|
onClick={handlerMaxAll}
|
||||||
alt=""
|
>
|
||||||
width={312}
|
{transI18n("maxAll")}
|
||||||
priority={true}
|
</button>
|
||||||
height={312}
|
</div>
|
||||||
style={{
|
|
||||||
filter: (theme === "winter" || theme === "cupcake") ? "invert(1)" : "none"
|
{/* Visual Skill Tree Container */}
|
||||||
}}
|
<div className="flex justify-center items-center py-2">
|
||||||
className={`w-full h-full object-cover rounded-xl`}
|
{traceButtons && avatarSelected && (
|
||||||
/>
|
<div className="relative w-full max-w-md aspect-square">
|
||||||
{traceButtons.map((btn, index) => {
|
<Image
|
||||||
if (!avatarSelected?.SkillTrees?.[btn.id]) {
|
unoptimized
|
||||||
return null
|
crossOrigin="anonymous"
|
||||||
}
|
src={`/skilltree/${avatarSelected?.BaseType?.toUpperCase()}.webp`}
|
||||||
return (
|
alt="Skill Tree"
|
||||||
<div
|
width={312}
|
||||||
key={`${btn.id} + ${index}`}
|
priority={true}
|
||||||
id={btn.id}
|
height={312}
|
||||||
className={`
|
style={{
|
||||||
absolute rounded-full border border-black
|
filter: (theme === "winter" || theme === "cupcake") ? "invert(1)" : "none"
|
||||||
bg-no-repeat bg-contain
|
}}
|
||||||
cursor-pointer transition-all duration-200 ease-in-out
|
className="w-full h-full object-cover"
|
||||||
shadow-[0_0_5px_white] flex justify-center items-center
|
/>
|
||||||
hover:scale-110z-10
|
{traceButtons.map((btn, index) => {
|
||||||
${btn.size === "small" ? "w-[6vw] h-[6vw] md:w-[2vw] md:h-[2vw] bg-white" : ""}
|
if (!avatarSelected?.SkillTrees?.[btn.id]) {
|
||||||
${btn.size === "medium" ? "w-[8vw] h-[8vw] md:w-[3vw] md:h-[3vw] bg-white" : ""}
|
return null
|
||||||
${btn.size === "big" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
|
}
|
||||||
${btn.size === "special" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-white" : ""}
|
const isSelected = skillIDSelected === btn.id;
|
||||||
${btn.size === "memory" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
|
const isLearned = !!avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID];
|
||||||
${btn.size === "elation" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
|
|
||||||
${skillIDSelected === btn.id ? "border-4 border-primary" : ""}
|
const isMajor = btn.size === "big" || btn.size === "memory" || btn.size === "elation";
|
||||||
${!avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID]
|
|
||||||
? "opacity-50 cursor-not-allowed"
|
return (
|
||||||
: ""}
|
<div
|
||||||
`}
|
key={`${btn.id}-${index}`}
|
||||||
onClick={() => {
|
id={btn.id}
|
||||||
setSkillIDSelected(btn.id === skillIDSelected ? null : btn.id)
|
className={`
|
||||||
}}
|
absolute rounded-full border border-black/70 cursor-pointer transition-all duration-150 ease-out flex justify-center items-center select-none overflow-visible
|
||||||
style={{
|
${btn.size === "small" ? "w-[6.5vw] h-[6.5vw] md:w-[2.2vw] md:h-[2.2vw] bg-white" : ""}
|
||||||
left: btn.left,
|
${btn.size === "medium" ? "w-[8.5vw] h-[8.5vw] md:w-[3vw] md:h-[3vw] bg-white" : ""}
|
||||||
top: btn.top,
|
${btn.size === "big" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
|
||||||
transform: "translate(-50%, -50%)",
|
${btn.size === "special" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-white" : ""}
|
||||||
}}
|
${btn.size === "memory" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
|
||||||
>
|
${btn.size === "elation" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
|
||||||
<Image
|
${isSelected ? "ring-3 ring-cyan-400 ring-offset-2 ring-offset-base-100 shadow-[0_0_12px_rgba(34,211,238,0.8)] z-20" : "hover:ring-2 hover:ring-primary/40 z-10"}
|
||||||
src={`${process.env.CDN_URL}/${avatarSelected?.SkillTrees?.[btn.id]?.["1"]?.Icon}`}
|
${!isLearned ? "opacity-40 grayscale" : "shadow-[0_0_6px_rgba(255,255,255,0.4)]"}
|
||||||
alt={btn.id.replaceAll("Point", "")}
|
`}
|
||||||
priority={true}
|
onClick={() => {
|
||||||
unoptimized
|
setSkillIDSelected(btn.id === skillIDSelected ? null : btn.id)
|
||||||
crossOrigin="anonymous"
|
|
||||||
width={124}
|
|
||||||
height={124}
|
|
||||||
style={{
|
|
||||||
objectFit: "contain",
|
|
||||||
padding: "2px",
|
|
||||||
filter: (btn.size !== "big" && btn.size !== "memory" && btn.size !== "elation") ? "brightness(0%)" : ""
|
|
||||||
}}
|
}}
|
||||||
/>
|
style={{
|
||||||
{(btn.size === "big" || btn.size === "memory" || btn.size === "elation") && (
|
left: btn.left,
|
||||||
<p className="
|
top: btn.top,
|
||||||
z-12 text-sm sm:text-xs lg:text-sm xl:text-base 2xl:text-2xl
|
transform: "translate(-50%, -50%)",
|
||||||
font-bold text-center rounded-full absolute
|
}}
|
||||||
translate-y-full mt-1
|
>
|
||||||
bg-base-300 px-1
|
<Image
|
||||||
left-1/2 transform -translate-x-1/2
|
src={`${process.env.CDN_URL}/${avatarSelected?.SkillTrees?.[btn.id]?.["1"]?.Icon}`}
|
||||||
">
|
alt={btn.id.replaceAll("Point", "")}
|
||||||
{`${avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] || 0}/${avatarSkillTree?.[btn.id]?.["1"]?.MaxLevel}`}
|
priority={true}
|
||||||
</p>
|
unoptimized
|
||||||
|
crossOrigin="anonymous"
|
||||||
)}
|
width={124}
|
||||||
{btn.size === "special" && (
|
height={124}
|
||||||
<div
|
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
objectFit: "contain",
|
||||||
inset: 0,
|
padding: "2px",
|
||||||
backgroundColor: "#4a6eff",
|
filter: !isMajor ? "brightness(0%)" : ""
|
||||||
mixBlendMode: "screen",
|
|
||||||
opacity: 0.8,
|
|
||||||
borderRadius: "50%"
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{btn.size === "memory" && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
inset: 0,
|
|
||||||
backgroundColor: "#9a89ff",
|
|
||||||
mixBlendMode: "screen",
|
|
||||||
opacity: 0.4,
|
|
||||||
borderRadius: "50%"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{btn.size === "elation" && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
inset: 0,
|
|
||||||
backgroundColor: "#ff8c00",
|
|
||||||
mixBlendMode: "screen",
|
|
||||||
opacity: 0.5,
|
|
||||||
borderRadius: "50%"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{btn.size === "big" && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
inset: 0,
|
|
||||||
backgroundColor: "#f5e4b0",
|
|
||||||
mixBlendMode: "screen",
|
|
||||||
opacity: 0.3,
|
|
||||||
borderRadius: "50%"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{/* Skill Type Color Overlays */}
|
||||||
|
{btn.size === "special" && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 rounded-full pointer-events-none"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#4a6eff",
|
||||||
|
mixBlendMode: "screen",
|
||||||
|
opacity: 0.8,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{btn.size === "memory" && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 rounded-full pointer-events-none"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#9a89ff",
|
||||||
|
mixBlendMode: "screen",
|
||||||
|
opacity: 0.45,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{btn.size === "elation" && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 rounded-full pointer-events-none"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#ff8c00",
|
||||||
|
mixBlendMode: "screen",
|
||||||
|
opacity: 0.5,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{btn.size === "big" && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 rounded-full pointer-events-none"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#f5e4b0",
|
||||||
|
mixBlendMode: "screen",
|
||||||
|
opacity: 0.35,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
)
|
{isMajor && (
|
||||||
})}
|
<p className="z-12 text-[10px] sm:text-xs font-mono font-bold text-center rounded-full absolute translate-y-full mt-0.5 bg-black/80 text-white px-1.5 py-0.2 border border-white/20 left-1/2 -translate-x-1/2 shadow-xs whitespace-nowrap">
|
||||||
</div>
|
{`${avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] || 0}/${avatarSkillTree?.[btn.id]?.["1"]?.MaxLevel}`}
|
||||||
)}
|
</p>
|
||||||
|
)}
|
||||||
{!traceButtons && avatarSelected && (
|
</div>
|
||||||
<div className="flex flex-col relative w-full aspect-square">
|
)
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
|
|
||||||
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
|
{/* Right Panel: Skill Details */}
|
||||||
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
|
<div className="lg:col-span-6 space-y-4">
|
||||||
{transI18n("details")}
|
<div className="glass-card rounded-2xl p-4 sm:p-6 border border-base-300 shadow-sm space-y-5">
|
||||||
</h2>
|
<div className="pb-3 border-b border-base-300">
|
||||||
{skillIDSelected && avatarSelected?.SkillTrees && avatarData && (
|
<h2 className="text-lg sm:text-xl font-bold flex items-center gap-2.5">
|
||||||
<div>
|
<div className="w-1.5 h-5 bg-linear-to-b from-secondary to-primary rounded-full"></div>
|
||||||
{skillInfo?.MaxLevel && skillInfo?.MaxLevel > 1 ? (
|
<span className="bg-clip-text text-transparent bg-linear-to-r from-secondary to-primary font-bold tracking-tight">
|
||||||
<div>
|
{transI18n("details")}
|
||||||
<div className="font-bold text-success">{transI18n("level")}</div>
|
</span>
|
||||||
<div className="w-full max-w-xs">
|
</h2>
|
||||||
<input type="range"
|
</div>
|
||||||
min={1}
|
|
||||||
max={skillInfo?.MaxLevel || 1}
|
{skillIDSelected && avatarSelected?.SkillTrees && avatarData ? (
|
||||||
value={avatarData?.data.skills?.[skillInfo?.PointID] || 1}
|
<div className="space-y-4">
|
||||||
onChange={(e) => {
|
{/* Level Slider */}
|
||||||
const newData = structuredClone(avatarData)
|
{skillInfo?.MaxLevel && skillInfo?.MaxLevel > 1 ? (
|
||||||
newData.data.skills[skillInfo?.PointID] = parseInt(e.target.value)
|
<div className="space-y-1.5 pb-2">
|
||||||
setAvatar(newData)
|
<div className="text-sm font-bold text-base-content/80">
|
||||||
}}
|
{transI18n("level")}
|
||||||
className="range range-success"
|
</div>
|
||||||
step="1" />
|
<div className="w-full max-w-sm space-y-1">
|
||||||
<div className="flex justify-between px-2.5 mt-2 text-xs">
|
<input
|
||||||
{Array.from({ length: skillInfo?.MaxLevel }, (_, index) => index + 1).map((index) => (
|
type="range"
|
||||||
<span key={index}>{index}</span>
|
min={1}
|
||||||
))}
|
max={skillInfo?.MaxLevel || 1}
|
||||||
|
value={avatarData?.data.skills?.[skillInfo?.PointID] || 1}
|
||||||
|
onChange={(e) => {
|
||||||
|
const newData = structuredClone(avatarData)
|
||||||
|
newData.data.skills[skillInfo?.PointID] = parseInt(e.target.value)
|
||||||
|
setAvatar(newData)
|
||||||
|
}}
|
||||||
|
className="range range-primary range-xs w-full"
|
||||||
|
step="1"
|
||||||
|
/>
|
||||||
|
<div className="flex justify-between px-1 text-[11px] font-mono text-base-content/50">
|
||||||
|
{Array.from({ length: skillInfo?.MaxLevel || 1 }, (_, index) => index + 1).map((idx) => (
|
||||||
|
<span key={idx}>{idx}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : skillInfo?.MaxLevel && skillInfo?.MaxLevel === 1 && traceButtons?.find((btn) => btn.id === skillIDSelected)?.size !== "big" ? (
|
||||||
) : skillInfo?.MaxLevel && skillInfo?.MaxLevel === 1 && traceButtons?.find((btn) => btn.id === skillIDSelected)?.size !== "big" ? (
|
<div className="flex items-center gap-3 pb-2">
|
||||||
<div className="flex items-center gap-2">
|
<span className="text-sm font-bold text-base-content/80">{transI18n("status")}</span>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={avatarData?.data.skills?.[skillInfo?.PointID] === 1}
|
checked={avatarData?.data.skills?.[skillInfo?.PointID] === 1}
|
||||||
className="toggle toggle-success"
|
className="toggle toggle-primary toggle-sm"
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (traceButtons?.find((btn) => btn.id === skillIDSelected)?.size === "special") {
|
if (traceButtons?.find((btn) => btn.id === skillIDSelected)?.size === "special") {
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
|
const newData = structuredClone(avatarData)
|
||||||
|
newData.data.skills[skillInfo?.PointID] = 1
|
||||||
|
setAvatar(newData)
|
||||||
|
return
|
||||||
|
}
|
||||||
const newData = structuredClone(avatarData)
|
const newData = structuredClone(avatarData)
|
||||||
newData.data.skills[skillInfo?.PointID] = 1
|
delete newData.data.skills[skillInfo?.PointID]
|
||||||
setAvatar(newData)
|
setAvatar(newData)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const newData = structuredClone(avatarData)
|
handlerChangeStatusTrace(e.target.checked)
|
||||||
delete newData.data.skills[skillInfo?.PointID]
|
}}
|
||||||
setAvatar(newData)
|
/>
|
||||||
return
|
<span className={`text-xs font-bold ${avatarData?.data.skills?.[skillInfo?.PointID] === 1 ? 'text-primary' : 'text-base-content/40'}`}>
|
||||||
}
|
{avatarData?.data.skills?.[skillInfo?.PointID] === 1 ? transI18n("active") : transI18n("inactive")}
|
||||||
handlerChangeStatusTrace(e.target.checked)
|
</span>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="font-bold text-success">
|
|
||||||
{avatarData?.data.skills?.[skillInfo?.PointID] === 1 ? transI18n("active") : transI18n("inactive")}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
) : (
|
|
||||||
null
|
|
||||||
)}
|
|
||||||
|
|
||||||
{((skillInfo?.PointName && skillInfo?.PointDesc) ||
|
{/* Point Name & Stat Additions */}
|
||||||
(skillInfo?.PointName && skillInfo?.StatusAddList.length > 0))
|
{((skillInfo?.PointName && skillInfo?.PointDesc) ||
|
||||||
&& (
|
(skillInfo?.PointName && skillInfo?.StatusAddList.length > 0)) && (
|
||||||
<div className="text-xl font-bold flex items-center gap-2 mt-2">
|
<div className="space-y-1.5">
|
||||||
{getLocaleName(locale, skillInfo.PointName)}
|
<h3 className="text-lg font-extrabold text-base-content">
|
||||||
|
{getLocaleName(locale, skillInfo.PointName)}
|
||||||
|
</h3>
|
||||||
{skillInfo.StatusAddList.length > 0 && (
|
{skillInfo.StatusAddList.length > 0 && (
|
||||||
<div>
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{skillInfo.StatusAddList.map((status, index) => (
|
{skillInfo.StatusAddList.map((status, index) => (
|
||||||
<div key={index}>
|
<span key={index} className="text-xs font-mono font-bold px-2 py-0.5 rounded-lg bg-primary/10 text-primary border border-primary/20">
|
||||||
<div className="text-xl font-bold">{getTraceBuffDisplay(status)}</div>
|
+{getTraceBuffDisplay(status)}
|
||||||
</div>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{skillInfo?.PointDesc && (
|
{/* Skill Description Prose */}
|
||||||
<div
|
{skillInfo?.PointDesc && (
|
||||||
dangerouslySetInnerHTML={{
|
<div
|
||||||
__html: replaceByParam(
|
className="text-xs sm:text-sm text-base-content/85 leading-relaxed"
|
||||||
getLocaleName(locale, skillInfo?.PointDesc) || "",
|
dangerouslySetInnerHTML={{
|
||||||
skillInfo?.Param || []
|
__html: replaceByParam(
|
||||||
)
|
getLocaleName(locale, skillInfo?.PointDesc) || "",
|
||||||
}}
|
skillInfo?.Param || []
|
||||||
/>
|
)
|
||||||
)}
|
}}
|
||||||
|
/>
|
||||||
{skillInfo?.LevelUpSkillID
|
)}
|
||||||
&& skillInfo?.LevelUpSkillID.length > 0
|
|
||||||
&& dataLevelUpSkill
|
|
||||||
&& (
|
|
||||||
<div className="mt-2 flex flex-col gap-2">
|
|
||||||
|
|
||||||
|
{/* Level-up Skill Items (Major / Servant Skills) */}
|
||||||
|
{skillInfo?.LevelUpSkillID && skillInfo?.LevelUpSkillID.length > 0 && dataLevelUpSkill && (
|
||||||
|
<div className="space-y-6 pt-1">
|
||||||
{dataLevelUpSkill?.data?.map((skill, index) => (
|
{dataLevelUpSkill?.data?.map((skill, index) => (
|
||||||
<div key={index}>
|
<div key={index} className={index > 0 ? "border-t border-base-content/10 pt-4" : ""}>
|
||||||
|
|
||||||
<div className="text-xl font-bold text-primary">
|
|
||||||
{transI18n(dataLevelUpSkill.isServant ? `${skill?.AttackType ? "severaltalent" : "servantskill"}` : `${skill?.AttackType ? skill?.AttackType.toLowerCase() : "talent"}`)}
|
|
||||||
{` (${transI18n(skill?.SkillEffect?.toLowerCase())})`}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SkillDescription
|
<SkillDescription
|
||||||
skill={skill}
|
skill={skill}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
avatarData={avatarData}
|
avatarData={avatarData}
|
||||||
skillInfo={skillInfo}
|
skillInfo={skillInfo}
|
||||||
|
attackTypeLabel={transI18n(dataLevelUpSkill.isServant ? (skill?.AttackType ? "severaltalent" : "servantskill") : (skill?.AttackType ? skill?.AttackType.toLowerCase() : "talent"))}
|
||||||
|
skillEffectLabel={skill?.SkillEffect ? transI18n(skill.SkillEffect.toLowerCase()) : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : (
|
||||||
|
<div className="text-center py-16 text-base-content/40 text-sm italic">
|
||||||
|
Select a trace node from the skill tree to configure
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,37 +2,54 @@ import { getLocaleName, replaceByParam } from "@/helper";
|
|||||||
import { AvatarStore, SkillDetail, SkillTreePoint } from "@/types";
|
import { AvatarStore, SkillDetail, SkillTreePoint } from "@/types";
|
||||||
import ExtraEffectList from "../extraInfo";
|
import ExtraEffectList from "../extraInfo";
|
||||||
|
|
||||||
export const SkillDescription = ({ skill, locale, avatarData, skillInfo }: {
|
export const SkillDescription = ({ skill, locale, avatarData, skillInfo, attackTypeLabel, skillEffectLabel }: {
|
||||||
skill: SkillDetail,
|
skill: SkillDetail,
|
||||||
locale: string,
|
locale: string,
|
||||||
avatarData: AvatarStore,
|
avatarData: AvatarStore,
|
||||||
skillInfo: SkillTreePoint
|
skillInfo: SkillTreePoint,
|
||||||
|
attackTypeLabel?: string,
|
||||||
|
skillEffectLabel?: string
|
||||||
}) => {
|
}) => {
|
||||||
const levelKey = avatarData?.data.skills?.[skillInfo?.PointID]?.toString() || "";
|
const levelKey = avatarData?.data.skills?.[skillInfo?.PointID]?.toString() || "";
|
||||||
const params = skill.Level[levelKey]?.Param || [];
|
const params = skill.Level[levelKey]?.Param || [];
|
||||||
const descHtml = getLocaleName(locale, skill.Desc) || getLocaleName(locale, skill.SimpleDesc);
|
const descHtml = getLocaleName(locale, skill.Desc) || getLocaleName(locale, skill.SimpleDesc);
|
||||||
|
|
||||||
const extraList = Object.values(skill.Extra).length > 0
|
const extraList = Object.values(skill.Extra || {}).length > 0
|
||||||
? skill.Extra
|
? skill.Extra
|
||||||
: skill?.SimpleExtra || {};
|
: skill?.SimpleExtra || {};
|
||||||
|
|
||||||
return (
|
const skillName = getLocaleName(locale, skill.Name);
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<div className="space-y-2 pb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-1 h-5 bg-primary/80 rounded-sm" />
|
|
||||||
<div
|
|
||||||
className="text-lg font-bold tracking-wide text-foreground uppercase"
|
|
||||||
dangerouslySetInnerHTML={{ __html: replaceByParam(getLocaleName(locale, skill.Name), []) }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
return (
|
||||||
className="text-[15px] leading-relaxed text-foreground/90 pl-3 border-l border-transparent"
|
<div className="space-y-2.5">
|
||||||
dangerouslySetInnerHTML={{ __html: replaceByParam(descHtml, params) }}
|
{/* Skill Name Heading */}
|
||||||
/>
|
<h3 className="text-xl font-bold text-base-content tracking-tight">
|
||||||
|
{skillName}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Attack Type & Effect Tag */}
|
||||||
|
{(attackTypeLabel || skillEffectLabel) && (
|
||||||
|
<div className="text-sm font-bold text-primary flex items-center gap-1.5">
|
||||||
|
<span>{attackTypeLabel}</span>
|
||||||
|
{skillEffectLabel && (
|
||||||
|
<span className="text-primary/70 font-medium">({skillEffectLabel})</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Skill Subtitle with accent bar */}
|
||||||
|
<div className="flex items-center gap-2 text-sm font-bold text-primary uppercase tracking-wide">
|
||||||
|
<span className="w-1 h-3.5 bg-primary rounded-xs"></span>
|
||||||
|
<span>{skillName}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div
|
||||||
|
className="text-xs sm:text-sm leading-relaxed text-base-content/85"
|
||||||
|
dangerouslySetInnerHTML={{ __html: replaceByParam(descHtml, params) }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Extra Effects Accordion */}
|
||||||
{Object.keys(extraList).length > 0 && (
|
{Object.keys(extraList).length > 0 && (
|
||||||
<ExtraEffectList extras={extraList} locale={locale} />
|
<ExtraEffectList extras={extraList} locale={locale} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { createContext, PropsWithChildren, useEffect } from "react";
|
import { createContext, PropsWithChildren, useCallback } from "react";
|
||||||
import useLocaleStore from "@/stores/localeStore";
|
import useLocaleStore from "@/stores/localeStore";
|
||||||
|
import { useShallow } from "zustand/react/shallow";
|
||||||
|
|
||||||
interface ThemeContextType {
|
interface ThemeContextType {
|
||||||
theme?: string;
|
theme?: string;
|
||||||
@@ -9,29 +10,20 @@ interface ThemeContextType {
|
|||||||
export const ThemeContext = createContext<ThemeContextType>({});
|
export const ThemeContext = createContext<ThemeContextType>({});
|
||||||
|
|
||||||
export const ThemeProvider = ({ children }: PropsWithChildren) => {
|
export const ThemeProvider = ({ children }: PropsWithChildren) => {
|
||||||
|
const { theme, setTheme } = useLocaleStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
theme: state.theme,
|
||||||
|
setTheme: state.setTheme,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
const { theme, setTheme } = useLocaleStore()
|
const changeTheme = useCallback((nextTheme: string | null) => {
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
const storedTheme = localStorage.getItem("theme");
|
|
||||||
if (storedTheme) setTheme(storedTheme);
|
|
||||||
}
|
|
||||||
}, [setTheme]);
|
|
||||||
|
|
||||||
const changeTheme = (nextTheme: string | null) => {
|
|
||||||
if (nextTheme) {
|
if (nextTheme) {
|
||||||
setTheme(nextTheme);
|
setTheme(nextTheme);
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
localStorage.setItem("theme", nextTheme);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setTheme(theme === "winter" ? "night" : "winter");
|
setTheme(theme === "winter" ? "night" : "winter");
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
localStorage.setItem("theme", theme);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
}, [theme, setTheme]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{ theme, changeTheme }}>
|
<ThemeContext.Provider value={{ theme, changeTheme }}>
|
||||||
|
|||||||
+10
-8
@@ -1,8 +1,10 @@
|
|||||||
export * from "./getName"
|
export * from "./getName";
|
||||||
export * from "./replaceByParam"
|
export * from "./replaceByParam";
|
||||||
export * from "./converterToAvatarStore"
|
export * from "./converterToAvatarStore";
|
||||||
export * from "./calcData"
|
export * from "./converterToFreeSRJson";
|
||||||
export * from "./random"
|
export * from "./getSkillTree";
|
||||||
export * from "./json"
|
export * from "./calcData";
|
||||||
export * from "./convertData"
|
export * from "./random";
|
||||||
export * from "./connect"
|
export * from "./json";
|
||||||
|
export * from "./convertData";
|
||||||
|
export * from "./connect";
|
||||||
+6
-7
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
import { ASGroupDetail, ChangelogItemType, AvatarDetail, FreeSRJson, LightConeDetail, MOCGroupDetail, MonsterDetail, PeakGroupDetail, PFGroupDetail, PSResponse, RelicSetDetail } from "@/types";
|
import { ASGroupDetail, ChangelogItemType, AvatarDetail, FreeSRJson, LightConeDetail, MOCGroupDetail, MonsterDetail, PeakGroupDetail, PFGroupDetail, PSResponse, RelicSetDetail } from "@/types";
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { psResponseSchema } from "@/zod";
|
import { psResponseSchema } from "@/zod";
|
||||||
@@ -108,7 +107,7 @@ export async function getChangelog(): Promise<ChangelogItemType[]> {
|
|||||||
const res = await axios.get<ChangelogItemType[]>(`/api/data/changelog`);
|
const res = await axios.get<ChangelogItemType[]>(`/api/data/changelog`);
|
||||||
return res.data;
|
return res.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch monster:', error);
|
console.error('Failed to fetch changelog:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,16 +126,16 @@ export async function SendDataToServer(
|
|||||||
return "Invalid response schema";
|
return "Invalid response schema";
|
||||||
}
|
}
|
||||||
return parsed.data;
|
return parsed.data;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
return error?.message || "Unknown error";
|
return (error as Error)?.message || "Unknown error";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function SendDataThroughProxy({ data }: { data: any }) {
|
export async function SendDataThroughProxy({ data }: { data: Record<string, unknown> | unknown }) {
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(`/api/proxy`, { ...data })
|
const response = await axios.post(`/api/proxy`, typeof data === "object" && data !== null ? { ...data } : data)
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,9 +4,12 @@ import { getASEventListApi } from '@/lib/api'
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import useDetailDataStore from '@/stores/detailDataStore'
|
import useDetailDataStore from '@/stores/detailDataStore'
|
||||||
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
|
||||||
export const useFetchASGroupData = () => {
|
export const useFetchASGroupData = () => {
|
||||||
const { setMapAS } = useDetailDataStore()
|
const { setMapAS } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({ setMapAS: state.setMapAS }))
|
||||||
|
)
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ['ASGroupData'],
|
queryKey: ['ASGroupData'],
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
|
||||||
"use client"
|
"use client"
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { getAvatarListApi } from '@/lib/api'
|
import { getAvatarListApi } from '@/lib/api'
|
||||||
@@ -8,26 +7,42 @@ import useDetailDataStore from '@/stores/detailDataStore'
|
|||||||
import useCurrentDataStore from '@/stores/currentDataStore'
|
import useCurrentDataStore from '@/stores/currentDataStore'
|
||||||
import useUserDataStore from '@/stores/userDataStore'
|
import useUserDataStore from '@/stores/userDataStore'
|
||||||
import { converterToAvatarStore } from '@/helper'
|
import { converterToAvatarStore } from '@/helper'
|
||||||
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
|
||||||
export const useFetchAvatarData = () => {
|
export const useFetchAvatarData = () => {
|
||||||
const { setMapAvatar, mapAvatar } = useDetailDataStore()
|
const { setMapAvatar, mapAvatar } = useDetailDataStore(
|
||||||
const { setAvatar, avatars } = useUserDataStore()
|
useShallow((state) => ({
|
||||||
const { avatarSelected, setAvatarSelected } = useCurrentDataStore()
|
setMapAvatar: state.setMapAvatar,
|
||||||
|
mapAvatar: state.mapAvatar,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { setAvatars, avatars } = useUserDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
setAvatars: state.setAvatars,
|
||||||
|
avatars: state.avatars,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { avatarSelected, setAvatarSelected } = useCurrentDataStore(
|
||||||
|
useShallow((state) => ({
|
||||||
|
avatarSelected: state.avatarSelected,
|
||||||
|
setAvatarSelected: state.setAvatarSelected,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ['AvatarData'],
|
queryKey: ['AvatarData'],
|
||||||
queryFn: getAvatarListApi,
|
queryFn: getAvatarListApi,
|
||||||
staleTime: 1000 * 60 * 5,
|
staleTime: 1000 * 60 * 5,
|
||||||
})
|
})
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listAvatarId = Object.keys(avatars)
|
if (!mapAvatar || Object.keys(mapAvatar).length === 0) return
|
||||||
const listAvatarNotExist = Object.entries(mapAvatar).filter(([avatarId]) => !listAvatarId.includes(avatarId))
|
const listAvatarId = Object.keys(avatars)
|
||||||
const avatarStore = converterToAvatarStore(Object.fromEntries(listAvatarNotExist))
|
const listAvatarNotExist = Object.entries(mapAvatar).filter(([avatarId]) => !listAvatarId.includes(avatarId))
|
||||||
if (Object.keys(avatarStore).length === 0) return
|
if (listAvatarNotExist.length === 0) return
|
||||||
for (const avatar of Object.values(avatarStore)) {
|
const avatarStore = converterToAvatarStore(Object.fromEntries(listAvatarNotExist))
|
||||||
setAvatar(avatar)
|
if (Object.keys(avatarStore).length === 0) return
|
||||||
}
|
setAvatars({ ...avatars, ...avatarStore })
|
||||||
}, [mapAvatar])
|
}, [mapAvatar, avatars, setAvatars])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (query.data) {
|
if (query.data) {
|
||||||
|
|||||||
@@ -5,10 +5,19 @@ import { useEffect } from 'react'
|
|||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import useModelStore from '@/stores/modelStore'
|
import useModelStore from '@/stores/modelStore'
|
||||||
import useLocaleStore from '@/stores/localeStore'
|
import useLocaleStore from '@/stores/localeStore'
|
||||||
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
|
||||||
export const useFetchChangelog = () => {
|
export const useFetchChangelog = () => {
|
||||||
const { currentVersion, setChangelog, setCurrentVersion } = useLocaleStore()
|
const { currentVersion, setChangelog, setCurrentVersion } = useLocaleStore(
|
||||||
const { setIsChangelog } = useModelStore()
|
useShallow((state) => ({
|
||||||
|
currentVersion: state.currentVersion,
|
||||||
|
setChangelog: state.setChangelog,
|
||||||
|
setCurrentVersion: state.setCurrentVersion,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
const { setIsChangelog } = useModelStore(
|
||||||
|
useShallow((state) => ({ setIsChangelog: state.setIsChangelog }))
|
||||||
|
)
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ['changelog'],
|
queryKey: ['changelog'],
|
||||||
queryFn: getChangelog,
|
queryFn: getChangelog,
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import { getLightconeListApi } from '@/lib/api';
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import useDetailDataStore from '@/stores/detailDataStore';
|
import useDetailDataStore from '@/stores/detailDataStore';
|
||||||
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
export const useFetchLightconeData = () => {
|
export const useFetchLightconeData = () => {
|
||||||
const { setMapLightCone } = useDetailDataStore()
|
const { setMapLightCone } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({ setMapLightCone: state.setMapLightCone }))
|
||||||
|
)
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ['lightconeData'],
|
queryKey: ['lightconeData'],
|
||||||
queryFn: getLightconeListApi,
|
queryFn: getLightconeListApi,
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import { getMOCEventListApi } from '@/lib/api'
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import useDetailDataStore from '@/stores/detailDataStore'
|
import useDetailDataStore from '@/stores/detailDataStore'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
|
||||||
export const useFetchMOCGroupData = () => {
|
export const useFetchMOCGroupData = () => {
|
||||||
const { setMapMoc } = useDetailDataStore()
|
const { setMapMoc } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({ setMapMoc: state.setMapMoc }))
|
||||||
|
)
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ['MOCGroupData'],
|
queryKey: ['MOCGroupData'],
|
||||||
queryFn: getMOCEventListApi,
|
queryFn: getMOCEventListApi,
|
||||||
|
|||||||
@@ -5,10 +5,15 @@ import { toast } from 'react-toastify'
|
|||||||
import { getMetadataApi } from '@/lib/api'
|
import { getMetadataApi } from '@/lib/api'
|
||||||
import useCurrentDataStore from '@/stores/currentDataStore'
|
import useCurrentDataStore from '@/stores/currentDataStore'
|
||||||
import useDetailDataStore from '@/stores/detailDataStore'
|
import useDetailDataStore from '@/stores/detailDataStore'
|
||||||
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
|
||||||
export const useFetchConfigData = () => {
|
export const useFetchConfigData = () => {
|
||||||
const { setMetaData } = useDetailDataStore()
|
const { setMetaData } = useDetailDataStore(
|
||||||
const { setSettingData } = useCurrentDataStore()
|
useShallow((state) => ({ setMetaData: state.setMetaData }))
|
||||||
|
)
|
||||||
|
const { setSettingData } = useCurrentDataStore(
|
||||||
|
useShallow((state) => ({ setSettingData: state.setSettingData }))
|
||||||
|
)
|
||||||
|
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ['initialConfigData'],
|
queryKey: ['initialConfigData'],
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import { getMonsterListApi } from '@/lib/api'
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import useDetailDataStore from '@/stores/detailDataStore'
|
import useDetailDataStore from '@/stores/detailDataStore'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
|
||||||
export const useFetchMonsterData = () => {
|
export const useFetchMonsterData = () => {
|
||||||
const { setMapMonster } = useDetailDataStore()
|
const { setMapMonster } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({ setMapMonster: state.setMapMonster }))
|
||||||
|
)
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ['MonsterData'],
|
queryKey: ['MonsterData'],
|
||||||
queryFn: getMonsterListApi,
|
queryFn: getMonsterListApi,
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import { getPeakEventListApi } from '@/lib/api'
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import useDetailDataStore from '@/stores/detailDataStore'
|
import useDetailDataStore from '@/stores/detailDataStore'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
|
||||||
export const useFetchPeakGroupData = () => {
|
export const useFetchPeakGroupData = () => {
|
||||||
const { setMapPeak } = useDetailDataStore()
|
const { setMapPeak } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({ setMapPeak: state.setMapPeak }))
|
||||||
|
)
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ['PeakGroupData'],
|
queryKey: ['PeakGroupData'],
|
||||||
queryFn: getPeakEventListApi,
|
queryFn: getPeakEventListApi,
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import { getPFEventListApi } from '@/lib/api'
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import useDetailDataStore from '@/stores/detailDataStore'
|
import useDetailDataStore from '@/stores/detailDataStore'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
|
||||||
export const useFetchPFGroupData = () => {
|
export const useFetchPFGroupData = () => {
|
||||||
const { setMapPF } = useDetailDataStore()
|
const { setMapPF } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({ setMapPF: state.setMapPF }))
|
||||||
|
)
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ['PFGroupData'],
|
queryKey: ['PFGroupData'],
|
||||||
queryFn: getPFEventListApi,
|
queryFn: getPFEventListApi,
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import { getRelicSetListApi } from '@/lib/api'
|
|||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import useDetailDataStore from '@/stores/detailDataStore'
|
import useDetailDataStore from '@/stores/detailDataStore'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
|
||||||
export const useFetchRelicSetData = () => {
|
export const useFetchRelicSetData = () => {
|
||||||
const { setMapRelicSet } = useDetailDataStore()
|
const { setMapRelicSet } = useDetailDataStore(
|
||||||
|
useShallow((state) => ({ setMapRelicSet: state.setMapRelicSet }))
|
||||||
|
)
|
||||||
const query = useQuery({
|
const query = useQuery({
|
||||||
queryKey: ['RelicSetData'],
|
queryKey: ['RelicSetData'],
|
||||||
queryFn: getRelicSetListApi,
|
queryFn: getRelicSetListApi,
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ const useCopyProfileStore = create<CopyProfileState>((set) => ({
|
|||||||
listElement: {},
|
listElement: {},
|
||||||
listPath: {},
|
listPath: {},
|
||||||
listRank: { "4": false, "5": false },
|
listRank: { "4": false, "5": false },
|
||||||
listCopyAvatar: [],
|
|
||||||
listRawCopyAvatar: [],
|
|
||||||
|
|
||||||
setListElement: (newListElement: Record<string, boolean>) => set({ listElement: newListElement }),
|
setListElement: (newListElement: Record<string, boolean>) => set({ listElement: newListElement }),
|
||||||
setListPath: (newListPath: Record<string, boolean>) => set({ listPath: newListPath }),
|
setListPath: (newListPath: Record<string, boolean>) => set({ listPath: newListPath }),
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ interface CurrentDataState {
|
|||||||
setMapAvatarPathActive: (mapAvatarPathActive: Record<string, boolean>) => void;
|
setMapAvatarPathActive: (mapAvatarPathActive: Record<string, boolean>) => void;
|
||||||
setMapLightconePathActive: (mapLightconePathActive: Record<string, boolean>) => void;
|
setMapLightconePathActive: (mapLightconePathActive: Record<string, boolean>) => void;
|
||||||
setMapLightconeRankActive: (mapLightconeRankActive: Record<string, boolean>) => void;
|
setMapLightconeRankActive: (mapLightconeRankActive: Record<string, boolean>) => void;
|
||||||
setResetDataLightcone: (avtarSeleted: AvatarDetail| null, baseType: Record<string, BaseTypeData>) => void;
|
setResetDataLightcone: (avatarSelected: AvatarDetail | null, baseType: Record<string, BaseTypeData>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useCurrentDataStore = create<CurrentDataState>((set) => ({
|
const useCurrentDataStore = create<CurrentDataState>((set) => ({
|
||||||
@@ -44,14 +44,16 @@ const useCurrentDataStore = create<CurrentDataState>((set) => ({
|
|||||||
setMapAvatarPathActive: (mapAvatarPathActive: Record<string, boolean>) => set({ mapAvatarPathActive }),
|
setMapAvatarPathActive: (mapAvatarPathActive: Record<string, boolean>) => set({ mapAvatarPathActive }),
|
||||||
setMapLightconePathActive: (mapLightconePathActive: Record<string, boolean>) => set({ mapLightconePathActive }),
|
setMapLightconePathActive: (mapLightconePathActive: Record<string, boolean>) => set({ mapLightconePathActive }),
|
||||||
setMapLightconeRankActive: (mapLightconeRankActive: Record<string, boolean>) => set({ mapLightconeRankActive }),
|
setMapLightconeRankActive: (mapLightconeRankActive: Record<string, boolean>) => set({ mapLightconeRankActive }),
|
||||||
setResetDataLightcone: (avtarSeleted: AvatarDetail| null, baseType: Record<string, BaseTypeData>) => {
|
setResetDataLightcone: (avatarSelected: AvatarDetail | null, baseType: Record<string, BaseTypeData>) => {
|
||||||
const newListPath: Record<string, boolean> = Object.fromEntries(Object.keys(baseType).map(key => [key, false]))
|
const newListPath: Record<string, boolean> = Object.fromEntries(Object.keys(baseType).map(key => [key, false]))
|
||||||
const newListRank: Record<string, boolean> = {"3": false, "4": false, "5": false }
|
const newListRank: Record<string, boolean> = {"3": false, "4": false, "5": false }
|
||||||
if (avtarSeleted) {
|
if (avatarSelected) {
|
||||||
newListPath[avtarSeleted.BaseType] = true
|
newListPath[avatarSelected.BaseType] = true
|
||||||
}
|
}
|
||||||
set({ mapLightconeRankActive: newListRank })
|
set({
|
||||||
set({ mapLightconePathActive: newListPath })
|
mapLightconeRankActive: newListRank,
|
||||||
|
mapLightconePathActive: newListPath,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { create } from 'zustand'
|
|||||||
interface ModelState {
|
interface ModelState {
|
||||||
isOpenLightcone: boolean;
|
isOpenLightcone: boolean;
|
||||||
isOpenRelic: boolean;
|
isOpenRelic: boolean;
|
||||||
isOpenAvatar: boolean;
|
|
||||||
isOpenCreateProfile: boolean;
|
isOpenCreateProfile: boolean;
|
||||||
isOpenImport: boolean;
|
isOpenImport: boolean;
|
||||||
isOpenCopy: boolean;
|
isOpenCopy: boolean;
|
||||||
@@ -21,7 +20,6 @@ interface ModelState {
|
|||||||
setIsOpenMonster: (newIsOpenMonster: boolean) => void;
|
setIsOpenMonster: (newIsOpenMonster: boolean) => void;
|
||||||
setIsOpenLightcone: (newIsOpenLightcone: boolean) => void;
|
setIsOpenLightcone: (newIsOpenLightcone: boolean) => void;
|
||||||
setIsOpenRelic: (newIsOpenRelic: boolean) => void;
|
setIsOpenRelic: (newIsOpenRelic: boolean) => void;
|
||||||
setIsOpenAvatar: (newIsOpenAvatar: boolean) => void;
|
|
||||||
setIsOpenCreateProfile: (newIsOpenCreateProfile: boolean) => void;
|
setIsOpenCreateProfile: (newIsOpenCreateProfile: boolean) => void;
|
||||||
setIsOpenImport: (newIsOpenImport: boolean) => void;
|
setIsOpenImport: (newIsOpenImport: boolean) => void;
|
||||||
setIsOpenCopy: (newIsOpenCopy: boolean) => void;
|
setIsOpenCopy: (newIsOpenCopy: boolean) => void;
|
||||||
@@ -31,7 +29,6 @@ interface ModelState {
|
|||||||
const useModelStore = create<ModelState>((set) => ({
|
const useModelStore = create<ModelState>((set) => ({
|
||||||
isOpenLightcone: false,
|
isOpenLightcone: false,
|
||||||
isOpenRelic: false,
|
isOpenRelic: false,
|
||||||
isOpenAvatar: false,
|
|
||||||
isOpenCreateProfile: false,
|
isOpenCreateProfile: false,
|
||||||
isOpenImport: false,
|
isOpenImport: false,
|
||||||
isOpenCopy: false,
|
isOpenCopy: false,
|
||||||
@@ -48,7 +45,6 @@ const useModelStore = create<ModelState>((set) => ({
|
|||||||
setIsOpenMonster: (newIsOpenMonster: boolean) => set({ isOpenMonster: newIsOpenMonster }),
|
setIsOpenMonster: (newIsOpenMonster: boolean) => set({ isOpenMonster: newIsOpenMonster }),
|
||||||
setIsOpenLightcone: (newIsOpenLightcone: boolean) => set({ isOpenLightcone: newIsOpenLightcone }),
|
setIsOpenLightcone: (newIsOpenLightcone: boolean) => set({ isOpenLightcone: newIsOpenLightcone }),
|
||||||
setIsOpenRelic: (newIsOpenRelic: boolean) => set({ isOpenRelic: newIsOpenRelic }),
|
setIsOpenRelic: (newIsOpenRelic: boolean) => set({ isOpenRelic: newIsOpenRelic }),
|
||||||
setIsOpenAvatar: (newIsOpenAvatar: boolean) => set({ isOpenAvatar: newIsOpenAvatar }),
|
|
||||||
setIsOpenCreateProfile: (newIsOpenCreateProfile: boolean) => set({ isOpenCreateProfile: newIsOpenCreateProfile }),
|
setIsOpenCreateProfile: (newIsOpenCreateProfile: boolean) => set({ isOpenCreateProfile: newIsOpenCreateProfile }),
|
||||||
setIsOpenImport: (newIsOpenImport: boolean) => set({ isOpenImport: newIsOpenImport }),
|
setIsOpenImport: (newIsOpenImport: boolean) => set({ isOpenImport: newIsOpenImport }),
|
||||||
setIsOpenCopy: (newIsOpenCopy: boolean) => set({ isOpenCopy: newIsOpenCopy }),
|
setIsOpenCopy: (newIsOpenCopy: boolean) => set({ isOpenCopy: newIsOpenCopy }),
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ const useRelicMakerStore = create<RelicMakerState>((set) => ({
|
|||||||
listSelectedSubStats: newSubAffixes.map(item => ({ ...item }))
|
listSelectedSubStats: newSubAffixes.map(item => ({ ...item }))
|
||||||
})),
|
})),
|
||||||
resetHistory: (index: number | null) => set((state) => {
|
resetHistory: (index: number | null) => set((state) => {
|
||||||
if (index !== null) {
|
if (index === null) {
|
||||||
return {
|
return {
|
||||||
preSelectedSubStats: {
|
preSelectedSubStats: {
|
||||||
"0": [
|
"0": [
|
||||||
@@ -166,47 +166,40 @@ const useRelicMakerStore = create<RelicMakerState>((set) => ({
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (index !== null) {
|
|
||||||
const newPreSelectedSubStats = { ...state.preSelectedSubStats };
|
|
||||||
newPreSelectedSubStats[index] = [
|
|
||||||
{
|
|
||||||
property: "",
|
|
||||||
affixId: "",
|
|
||||||
rollCount: 0,
|
|
||||||
stepCount: 0
|
|
||||||
},
|
|
||||||
]
|
|
||||||
return {
|
|
||||||
preSelectedSubStats: newPreSelectedSubStats
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return {}
|
const newPreSelectedSubStats = { ...state.preSelectedSubStats };
|
||||||
|
newPreSelectedSubStats[index.toString()] = [
|
||||||
|
{
|
||||||
|
property: "",
|
||||||
|
affixId: "",
|
||||||
|
rollCount: 0,
|
||||||
|
stepCount: 0
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
preSelectedSubStats: newPreSelectedSubStats
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
popHistory: (index: number) => set((state) => {
|
popHistory: (index: number) => set((state) => {
|
||||||
|
const key = index.toString();
|
||||||
|
const currentList = state.preSelectedSubStats[key];
|
||||||
|
if (!currentList || currentList.length <= 1) return {};
|
||||||
const newPreSelectedSubStats = structuredClone(state.preSelectedSubStats);
|
const newPreSelectedSubStats = structuredClone(state.preSelectedSubStats);
|
||||||
const copied = state.preSelectedSubStats[index].map(item => ({ ...item }));
|
const copied = currentList.map(item => ({ ...item }));
|
||||||
copied.pop();
|
copied.pop();
|
||||||
newPreSelectedSubStats[index] = copied;
|
newPreSelectedSubStats[key] = copied;
|
||||||
return { preSelectedSubStats: newPreSelectedSubStats };
|
return { preSelectedSubStats: newPreSelectedSubStats };
|
||||||
}),
|
}),
|
||||||
addHistory: (index, affix) => set((state) => {
|
addHistory: (index, affix) => set((state) => {
|
||||||
|
const key = index.toString();
|
||||||
const newPreSelectedSubStats = structuredClone(state.preSelectedSubStats);
|
const newPreSelectedSubStats = structuredClone(state.preSelectedSubStats);
|
||||||
|
const currentList = state.preSelectedSubStats[key] || [
|
||||||
const currentList = state.preSelectedSubStats[index];
|
{ property: "", affixId: "", rollCount: 0, stepCount: 0 }
|
||||||
const copied = currentList ? currentList.map(item => ({ ...item })) : [
|
|
||||||
{ property: "", affixId: "", rollCount: 0, stepCount: 0 },
|
|
||||||
{ property: "", affixId: "", rollCount: 0, stepCount: 0 },
|
|
||||||
{ property: "", affixId: "", rollCount: 0, stepCount: 0 },
|
|
||||||
{ property: "", affixId: "", rollCount: 0, stepCount: 0 },
|
|
||||||
];
|
];
|
||||||
|
const copied = currentList.map(item => ({ ...item }));
|
||||||
if (copied) {
|
copied.push(structuredClone(affix));
|
||||||
const newAffix = structuredClone(affix);
|
newPreSelectedSubStats[key] = copied;
|
||||||
copied.push(newAffix);
|
return { preSelectedSubStats: newPreSelectedSubStats };
|
||||||
newPreSelectedSubStats[index] = copied;
|
|
||||||
return { preSelectedSubStats: newPreSelectedSubStats };
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const useUserDataStore = create<UserDataState>()(
|
|||||||
moc_config: {
|
moc_config: {
|
||||||
event_id: 0,
|
event_id: 0,
|
||||||
challenge_id: 0,
|
challenge_id: 0,
|
||||||
floor_side: "Upper",
|
floor_side: "firstNode",
|
||||||
use_turbulence_buff: true,
|
use_turbulence_buff: true,
|
||||||
use_cycle_count: true,
|
use_cycle_count: true,
|
||||||
blessings: [],
|
blessings: [],
|
||||||
@@ -54,7 +54,7 @@ const useUserDataStore = create<UserDataState>()(
|
|||||||
event_id: 0,
|
event_id: 0,
|
||||||
challenge_id: 0,
|
challenge_id: 0,
|
||||||
buff_id: 0,
|
buff_id: 0,
|
||||||
floor_side: "Upper",
|
floor_side: "firstNode",
|
||||||
blessings: [],
|
blessings: [],
|
||||||
cycle_count: 0,
|
cycle_count: 0,
|
||||||
stage_id: 0,
|
stage_id: 0,
|
||||||
@@ -64,7 +64,7 @@ const useUserDataStore = create<UserDataState>()(
|
|||||||
event_id: 0,
|
event_id: 0,
|
||||||
challenge_id: 0,
|
challenge_id: 0,
|
||||||
buff_id: 0,
|
buff_id: 0,
|
||||||
floor_side: "Upper",
|
floor_side: "firstNode",
|
||||||
blessings: [],
|
blessings: [],
|
||||||
cycle_count: 0,
|
cycle_count: 0,
|
||||||
stage_id: 0,
|
stage_id: 0,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ExtraEffect } from "./avatarDetail";
|
import { ExtraEffect } from "./avatarDetail";
|
||||||
import { InfiniteWave, MazeBuff } from "./pfDetail";
|
import { InfiniteWave, Invasion, MazeBuff } from "./pfDetail";
|
||||||
|
|
||||||
export interface ASGroupDetail {
|
export interface ASGroupDetail {
|
||||||
ID: number;
|
ID: number;
|
||||||
@@ -64,6 +64,7 @@ export interface ASEvent {
|
|||||||
Level: number;
|
Level: number;
|
||||||
Release: boolean;
|
Release: boolean;
|
||||||
MonsterList: number[][];
|
MonsterList: number[][];
|
||||||
|
Invasion: Invasion | null;
|
||||||
Infinite: InfiniteWave[] | null;
|
Infinite: InfiniteWave[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
declare module '*.css';
|
||||||
|
declare module '*.scss';
|
||||||
|
declare module '*.module.css';
|
||||||
|
declare module '*.module.scss';
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { InfiniteWave, MazeBuff } from "./pfDetail";
|
import { InfiniteWave, Invasion, MazeBuff } from "./pfDetail";
|
||||||
|
|
||||||
export interface MOCGroupDetail {
|
export interface MOCGroupDetail {
|
||||||
ID: number;
|
ID: number;
|
||||||
@@ -40,6 +40,7 @@ export interface MoCTarget {
|
|||||||
Param: number[];
|
Param: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface MoCEvent {
|
export interface MoCEvent {
|
||||||
ID: number;
|
ID: number;
|
||||||
Name: Record<string, string>;
|
Name: Record<string, string>;
|
||||||
@@ -48,5 +49,6 @@ export interface MoCEvent {
|
|||||||
Level: number;
|
Level: number;
|
||||||
Release: boolean;
|
Release: boolean;
|
||||||
MonsterList: number[][];
|
MonsterList: number[][];
|
||||||
|
Invasion: Invasion | null;
|
||||||
Infinite: InfiniteWave[] | null;
|
Infinite: InfiniteWave[] | null;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { BattleTarget, InfiniteWave, MazeBuff } from "./pfDetail";
|
import { BattleTarget, InfiniteWave, Invasion, MazeBuff } from "./pfDetail";
|
||||||
|
|
||||||
export interface PeakGroupDetail {
|
export interface PeakGroupDetail {
|
||||||
ID: number;
|
ID: number;
|
||||||
@@ -37,5 +37,6 @@ export interface PeakEvent {
|
|||||||
Level: number;
|
Level: number;
|
||||||
Release: boolean;
|
Release: boolean;
|
||||||
MonsterList: number[][];
|
MonsterList: number[][];
|
||||||
|
Invasion: Invasion | null;
|
||||||
Infinite: InfiniteWave[] | null;
|
Infinite: InfiniteWave[] | null;
|
||||||
}
|
}
|
||||||
+13
-7
@@ -18,6 +18,12 @@ export interface MazeBuff {
|
|||||||
Desc: Record<string, string>;
|
Desc: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Invasion {
|
||||||
|
ID: number;
|
||||||
|
MazeBuff: MazeBuff;
|
||||||
|
Desc: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PFTierceLevel {
|
export interface PFTierceLevel {
|
||||||
ID: number;
|
ID: number;
|
||||||
PreChallenge: number;
|
PreChallenge: number;
|
||||||
@@ -55,7 +61,12 @@ export interface BattleTarget {
|
|||||||
Param: number[];
|
Param: number[];
|
||||||
Name: Record<string, string>;
|
Name: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
export interface InfiniteWave {
|
||||||
|
ID: number;
|
||||||
|
MaxMonsterCount: number;
|
||||||
|
MonsterList: number[];
|
||||||
|
EliteGroup: number;
|
||||||
|
}
|
||||||
export interface PFEvent {
|
export interface PFEvent {
|
||||||
ID: number;
|
ID: number;
|
||||||
Name: Record<string, string>;
|
Name: Record<string, string>;
|
||||||
@@ -64,12 +75,7 @@ export interface PFEvent {
|
|||||||
Level: number;
|
Level: number;
|
||||||
Release: boolean;
|
Release: boolean;
|
||||||
MonsterList: number[][];
|
MonsterList: number[][];
|
||||||
|
Invasion: Invasion | null;
|
||||||
Infinite?: InfiniteWave[] | null;
|
Infinite?: InfiniteWave[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InfiniteWave {
|
|
||||||
ID: number;
|
|
||||||
MaxMonsterCount: number;
|
|
||||||
MonsterList: number[];
|
|
||||||
EliteGroup: number;
|
|
||||||
}
|
|
||||||
@@ -25,11 +25,11 @@ export interface LightconeJson {
|
|||||||
export interface AvatarData {
|
export interface AvatarData {
|
||||||
rank: number,
|
rank: number,
|
||||||
skills: Record<string, number>
|
skills: Record<string, number>
|
||||||
skills_by_anchor_type?: Record<string,number>
|
skills_by_anchor_type?: Record<string, number> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AvatarJson {
|
export interface AvatarJson {
|
||||||
owner_uid?: number;
|
owner_uid?: number | null;
|
||||||
avatar_id: number;
|
avatar_id: number;
|
||||||
data: AvatarData;
|
data: AvatarData;
|
||||||
level: number;
|
level: number;
|
||||||
@@ -37,12 +37,12 @@ export interface AvatarJson {
|
|||||||
techniques: number[];
|
techniques: number[];
|
||||||
sp_value: number;
|
sp_value: number;
|
||||||
sp_max: number;
|
sp_max: number;
|
||||||
enhanced_id?: number;
|
enhanced_id?: number | null;
|
||||||
}
|
}
|
||||||
export interface MonsterJson {
|
export interface MonsterJson {
|
||||||
monster_id: number;
|
monster_id: number;
|
||||||
level: number;
|
level: number;
|
||||||
amount: number;
|
amount?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DynamicKeyJson {
|
export interface DynamicKeyJson {
|
||||||
@@ -53,7 +53,7 @@ export interface DynamicKeyJson {
|
|||||||
export interface BattleBuffJson {
|
export interface BattleBuffJson {
|
||||||
level: number;
|
level: number;
|
||||||
id: number;
|
id: number;
|
||||||
dynamic_key?: DynamicKeyJson;
|
dynamic_key?: DynamicKeyJson | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BattleConfigJson {
|
export interface BattleConfigJson {
|
||||||
@@ -71,12 +71,12 @@ type LoadoutJson = {
|
|||||||
relic_list: string[]
|
relic_list: string[]
|
||||||
}
|
}
|
||||||
export interface FreeSRJson {
|
export interface FreeSRJson {
|
||||||
key?: string;
|
key?: string | null;
|
||||||
lightcones: LightconeJson[];
|
lightcones: LightconeJson[];
|
||||||
relics: RelicJson[];
|
relics: RelicJson[];
|
||||||
avatars: Record<string, AvatarJson>;
|
avatars: Record<string, AvatarJson>;
|
||||||
battle_config: BattleConfigJson;
|
battle_config: BattleConfigJson;
|
||||||
loadout?: LoadoutJson[];
|
loadout?: LoadoutJson[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ export const lightconeJsonSchema = z.object({
|
|||||||
export const avatarDataSchema = z.object({
|
export const avatarDataSchema = z.object({
|
||||||
rank: z.number(),
|
rank: z.number(),
|
||||||
skills: z.record(z.string(), z.number()),
|
skills: z.record(z.string(), z.number()),
|
||||||
skills_by_anchor_type: z.record(z.string(), z.number()).optional()
|
skills_by_anchor_type: z.record(z.string(), z.number()).nullish()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const avatarJsonSchema = z.object({
|
export const avatarJsonSchema = z.object({
|
||||||
owner_uid: z.number().optional(),
|
owner_uid: z.number().nullish(),
|
||||||
avatar_id: z.number(),
|
avatar_id: z.number(),
|
||||||
data: avatarDataSchema,
|
data: avatarDataSchema,
|
||||||
level: z.number(),
|
level: z.number(),
|
||||||
@@ -41,13 +41,13 @@ export const avatarJsonSchema = z.object({
|
|||||||
techniques: z.array(z.number()),
|
techniques: z.array(z.number()),
|
||||||
sp_value: z.number(),
|
sp_value: z.number(),
|
||||||
sp_max: z.number(),
|
sp_max: z.number(),
|
||||||
enhanced_id: z.number().optional()
|
enhanced_id: z.number().nullish()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const monsterJsonSchema = z.object({
|
export const monsterJsonSchema = z.object({
|
||||||
monster_id: z.number(),
|
monster_id: z.number(),
|
||||||
level: z.number(),
|
level: z.number(),
|
||||||
amount: z.number()
|
amount: z.number().nullish()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const dynamicKeyJsonSchema = z.object({
|
export const dynamicKeyJsonSchema = z.object({
|
||||||
@@ -58,7 +58,7 @@ export const dynamicKeyJsonSchema = z.object({
|
|||||||
export const battleBuffJsonSchema = z.object({
|
export const battleBuffJsonSchema = z.object({
|
||||||
level: z.number(),
|
level: z.number(),
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
dynamic_key: dynamicKeyJsonSchema.optional()
|
dynamic_key: dynamicKeyJsonSchema.nullish()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const battleConfigJsonSchema = z.object({
|
export const battleConfigJsonSchema = z.object({
|
||||||
@@ -78,12 +78,12 @@ const loadoutJsonSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const freeSrJsonSchema = z.object({
|
export const freeSrJsonSchema = z.object({
|
||||||
key: z.string().optional(),
|
key: z.string().nullish(),
|
||||||
lightcones: z.array(lightconeJsonSchema),
|
lightcones: z.array(lightconeJsonSchema),
|
||||||
relics: z.array(relicJsonSchema),
|
relics: z.array(relicJsonSchema),
|
||||||
avatars: z.record(z.string(), avatarJsonSchema),
|
avatars: z.record(z.string(), avatarJsonSchema),
|
||||||
battle_config: battleConfigJsonSchema,
|
battle_config: battleConfigJsonSchema,
|
||||||
loadout: z.array(loadoutJsonSchema).optional()
|
loadout: z.array(loadoutJsonSchema).nullish()
|
||||||
});
|
});
|
||||||
|
|
||||||
const extraDataSchema = z.any();
|
const extraDataSchema = z.any();
|
||||||
@@ -91,5 +91,5 @@ const extraDataSchema = z.any();
|
|||||||
export const psResponseSchema = z.object({
|
export const psResponseSchema = z.object({
|
||||||
status: z.number(),
|
status: z.number(),
|
||||||
message: z.string(),
|
message: z.string(),
|
||||||
extra_data: extraDataSchema.optional()
|
extra_data: extraDataSchema.nullish()
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user