Refactor TypeScript interfaces and types for improved readability and consistency
Gitea Auto Deploy / Deploy-Container (push) Failing after 13m19s

- Reformatted interfaces in changelog.ts, enka.ts, extraData.ts, global.d.ts, index.ts, lightconeDetail.ts, metaData.ts, mics.ts, mocDetail.ts, modelConfig.ts, monsterDetail.ts, peakDetail.ts, pfDetail.ts, psConnect.ts, relicDetail.ts, showcase.ts, srtools.ts, and zod/index.ts for consistent indentation and spacing.
- Added missing imports and interfaces where necessary, such as Invasion in mocDetail.ts and peakDetail.ts.
- Ensured all interfaces are properly structured and maintain a consistent style across the codebase.
This commit is contained in:
2026-08-27 00:38:15 +07:00
parent faafd9aecf
commit 2a36904b43
131 changed files with 16135 additions and 15014 deletions
+1
View File
@@ -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
+299 -293
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
+1
View File
@@ -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",
+3 -1
View File
@@ -286,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"
} }
} }
+3 -1
View File
@@ -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",
@@ -286,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"
} }
} }
+3 -1
View File
@@ -286,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"
} }
} }
+3 -1
View File
@@ -286,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"
} }
} }
+3 -1
View File
@@ -286,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"
} }
} }
+3 -1
View File
@@ -285,6 +285,8 @@
"trailblazer": "開拓者", "trailblazer": "開拓者",
"listExtraEffect": "追加効果一覧", "listExtraEffect": "追加効果一覧",
"extra": "追加", "extra": "追加",
"customLineup": "カスタム編成" "customLineup": "カスタム編成",
"def": "防御力",
"language": "言語"
} }
} }
+3 -1
View File
@@ -285,6 +285,8 @@
"trailblazer": "개척자", "trailblazer": "개척자",
"listExtraEffect": "추가 효과 목록", "listExtraEffect": "추가 효과 목록",
"extra": "추가", "extra": "추가",
"customLineup": "커스텀 편성" "customLineup": "커스텀 편성",
"def": "방어력",
"language": "언어"
} }
} }
+3 -1
View File
@@ -286,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"
} }
} }
+3 -1
View File
@@ -286,6 +286,8 @@
"trailblazer": "Первопроходец", "trailblazer": "Первопроходец",
"listExtraEffect": "Список доп. эффектов", "listExtraEffect": "Список доп. эффектов",
"extra": "Экстра", "extra": "Экстра",
"customLineup": "Пользовательский отряд" "customLineup": "Пользовательский отряд",
"def": "Защ",
"language": "Язык"
} }
} }
+3 -1
View File
@@ -286,6 +286,8 @@
"trailblazer": "ผู้บุกเบิก", "trailblazer": "ผู้บุกเบิก",
"listExtraEffect": "รายการเอฟเฟกต์พิเศษ", "listExtraEffect": "รายการเอฟเฟกต์พิเศษ",
"extra": "พิเศษ", "extra": "พิเศษ",
"customLineup": "จัดทีมแบบกำหนดเอง" "customLineup": "จัดทีมแบบกำหนดเอง",
"def": "DEF",
"language": "ภาษา"
} }
} }
+4 -2
View File
@@ -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",
@@ -285,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ữ"
} }
} }
+3 -1
View File
@@ -285,6 +285,8 @@
"trailblazer": "开拓者", "trailblazer": "开拓者",
"listExtraEffect": "额外效果列表", "listExtraEffect": "额外效果列表",
"extra": "额外", "extra": "额外",
"customLineup": "自定义阵容" "customLineup": "自定义阵容",
"def": "防御力",
"language": "语言"
} }
} }
+12 -12
View File
@@ -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"
}, },
+125
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
); );
} }
+140 -81
View File
@@ -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
View File
@@ -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>
); );
} }
+77 -44
View File
@@ -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>
); );
} }
+108 -76
View File
@@ -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 !== 0 && ( {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>
)} )}
+33 -31
View File
@@ -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>
); );
} }
+44 -33
View File
@@ -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>
+82 -143
View File
@@ -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>
) )
} }
+58 -43
View File
@@ -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>
); );
}; };
+37 -42
View File
@@ -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
View File
@@ -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>
) )
} }
+101 -44
View File
@@ -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>
+410 -396
View File
@@ -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>
) )
} }
+13 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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) => {
+82 -45
View File
@@ -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 && (
+74 -40
View File
@@ -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 && (
+134 -67
View File
@@ -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>
))} ))}
+33 -10
View File
@@ -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) => (
+75 -69
View File
@@ -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>
); );
+49 -33
View File
@@ -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>
+34 -10
View File
@@ -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
View File
@@ -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>
);
} }
+37 -16
View File
@@ -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) => (
+125 -92
View File
@@ -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
View File
@@ -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
View File
@@ -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>
); );
} }
+31 -7
View File
@@ -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 -2
View File
@@ -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={{
+65 -36
View File
@@ -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>
); );
} }
+71 -85
View File
@@ -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
View File
@@ -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>
+34 -17
View File
@@ -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} />
)} )}
+10 -18
View File
@@ -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
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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'],
+28 -13
View File
@@ -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) {
+11 -2
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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,
+7 -2
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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,
-2
View File
@@ -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 }),
+8 -6
View File
@@ -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
View File
@@ -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 }),
+25 -32
View File
@@ -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 {}
}), }),
})); }));
+3 -3
View File
@@ -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,
+2 -1
View File
@@ -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;
} }
+3 -1
View File
@@ -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;
} }
+2 -1
View File
@@ -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
View File
@@ -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;
}