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"
}
}
}
+78 -6
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;
@@ -12,16 +11,16 @@
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);
}
}
+10 -8
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 />
<div className="flex-1 p-2 sm:p-3 sm:pt-1">
{children} {children}
</div> </div>
</div> </section>
</main>
<Footer /> <Footer />
</div> </div>
+228 -163
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,77 +243,76 @@ 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">
<div className="flex flex-col justify-center w-full"> {/* Character Identity */}
<div className="flex flex-wrap items-center gap-2 ">
<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">
{avatarSelected && ( {avatarSelected && (
<div className="flex flex-wrap items-center justify-start h-full w-full"> <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 <Image
src={`${process.env.CDN_URL}/${damageType?.[avatarSelected?.DamageType]?.Icon}`} src={`${process.env.CDN_URL}/${damageType?.[avatarSelected?.DamageType]?.Icon}`}
alt={'damage type'} alt="damage type"
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
className="h-10 w-10 object-contain" className="h-5 w-5 object-contain"
width={100} width={20}
height={100} height={20}
/> />
<p className="text-center font-bold text-lg"> <span className="text-base-content/85 font-medium">{transI18n(avatarSelected.BaseType.toLowerCase())}</span>
{transI18n(avatarSelected.BaseType.toLowerCase())} <span className="text-base-content/30">/</span>
</p>
<div className="text-center font-bold text-lg">{" / "}</div>
<ParseText <ParseText
locale={locale} locale={locale}
text={getNameChar(locale, transI18n, avatarSelected).toWellFormed()} text={getNameChar(locale, transI18n, avatarSelected).toWellFormed()}
className={"font-bold text-lg"} className="font-bold text-primary"
/> />
<div className="text-center italic text-sm ml-2"> {`(${avatarSelected.ID})`}</div> <span className="text-[11px] font-mono text-base-content/60 font-semibold">#{avatarSelected.ID}</span>
</div> </div>
)} )}
</div>
</div> {/* Profile Selector & Quick Actions */}
<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 gap-2"> <div className="dropdown dropdown-bottom dropdown-start h-full flex items-center">
<span className="text-base opacity-70 font-bold w-16">{transI18n("profile")}:</span>
<div className="dropdown dropdown-center md:dropdown-start">
<div <div
tabIndex={0} tabIndex={0}
role="button" role="button"
className="btn btn-warning border-info btn-soft gap-1" 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"
> >
<span className="truncate max-w-48 font-bold"> <span className="text-[11px] uppercase tracking-wider text-base-content/60 font-semibold">{transI18n("profile")}:</span>
{profileCurrent?.profile_name} <span className="truncate max-w-28 sm:max-w-40 text-xs text-base-content font-bold">
{profileCurrent?.profile_name || "Default Profile"}
</span> </span>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <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" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg> </svg>
</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"> <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) => ( {listProfile.map((profile, index) => (
<li key={index} className="grid grid-cols-12"> <li key={index} className="flex flex-row items-center justify-between rounded-xl px-1 hover:bg-base-200">
<button <button
className={`col-span-8 btn btn-ghost`} className="flex-1 text-left py-2 px-2 text-xs font-medium truncate"
onClick={() => handleProfileSelect(index)} onClick={() => handleProfileSelect(index)}
> >
<span className="flex-1 truncate text-left"> <span className="truncate">{profile.profile_name}</span>
{profile.profile_name}
{index === 0 && ( {index === 0 && (
<span className="text-xs text-base-content/60 ml-1">({transI18n("default")})</span> <span className="text-[10px] text-base-content/50 ml-1.5 font-mono">({transI18n("default")})</span>
)} )}
</span>
</button> </button>
{index !== 0 && ( {index !== 0 && (
<> <div className="flex items-center gap-1">
<button <button
className="col-span-2 flex items-center justify-center text-lg text-warning hover:bg-warning/20 z-20 w-full h-full" className="btn btn-ghost btn-xs btn-circle text-warning hover:bg-warning/20"
onClick={() => { onClick={() => {
setFormState("EDIT"); setFormState("EDIT");
setProfileName(profile.profile_name) setProfileName(profile.profile_name)
@@ -288,158 +322,189 @@ export default function ActionBar() {
}} }}
title="Edit Profile" title="Edit Profile"
> >
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24"> <Edit2 className="w-3 h-3" />
<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>
<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" className="btn btn-ghost btn-xs btn-circle text-error hover:bg-error/20"
onClick={(e) => { onClick={(e) => handleDeleteProfile(index, e)}
handleDeleteProfile(index, e)
}}
title="Delete Profile" title="Delete Profile"
> >
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24"> <Trash2 className="w-3 h-3" />
<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> </button>
</> </div>
)} )}
</li> </li>
))} ))}
<li className="border-t border-base-300 mt-2 pt-2 z-10"> <li className="border-t border-base-300 pt-1 mt-1">
<button <button
onClick={() => { onClick={() => {
setIsOpenCopy(true) setIsOpenCopy(true)
setResetData(baseType, damageType) setResetData(baseType, damageType)
handleShow("copy_profile_modal") 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" className="flex items-center gap-2 py-2 px-3 text-xs font-semibold text-primary hover:bg-primary/10 rounded-xl"
> >
<svg className="w-4 h-4" 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="M12 4v16m8-8H4" />
</svg>
{transI18n("copyProfiles")} {transI18n("copyProfiles")}
</button> </button>
</li>
<li>
<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={() => { onClick={() => {
setIsOpenCreateProfile(true) setIsOpenCreateProfile(true)
setFormState("CREATE"); setFormState("CREATE");
setProfileName("") setProfileName("")
handleShow("update_profile_modal") 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"
> >
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <Plus className="w-3.5 h-3.5" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
{transI18n("addNewProfile")} {transI18n("addNewProfile")}
</button> </button>
</li> </li>
</ul> </ul>
</div> </div>
<div className=" grid grid-cols-2 w-full sm:hidden gap-2">
{/* Quick Profile Actions */}
<div className="flex items-center border-l border-base-content/10 pl-1 pr-0.5 gap-0.5 h-5">
<button <button
onClick={() => { onClick={() => {
setIsOpenAvatars(true) setIsOpenCopy(true)
handleShow("avatars_modal") setResetData(baseType, damageType)
handleShow("copy_profile_modal")
}} }}
className="col-span-1 btn btn-warning btn-sm w-full"> 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("copyProfiles")}
>
<Copy className="w-3.5 h-3.5" />
</button>
<button
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>
{/* Desktop Connect PS Button */}
<div className="hidden sm:flex items-center gap-2 ml-auto tooltip tooltip-bottom" data-tip={isConnectPS ? transI18n("sync") : transI18n("connectPs")}>
<button
onClick={handleConnectOrSyncPS}
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 ${
isConnectPS
? "btn-outline btn-accent"
: "btn-primary shadow-sm"
}`}
>
<RefreshCw className={`w-3.5 h-3.5 ${isConnectPS ? "text-accent" : ""}`} />
<span className="text-xs">{isConnectPS ? transI18n("sync") : transI18n("connectPs")}</span>
</button>
</div>
</div>
{/* Mobile-Only Action Row (< 640px): [Avatars Button] & [Actions Dropdown] */}
<div className="grid grid-cols-2 sm:hidden gap-2 w-full pt-1">
<button
onClick={() => {
setIsOpenAvatars(true);
handleShow("avatars_modal");
}}
className="btn btn-warning btn-sm w-full font-bold shadow-xs"
>
{transI18n("avatars")} {transI18n("avatars")}
</button> </button>
<div className="col-span-1 dropdown dropdown-center w-full">
<label tabIndex={0} className="btn btn-info btn-sm w-full"> <div className="dropdown dropdown-end w-full">
<label tabIndex={0} className="btn btn-info btn-sm w-full font-bold shadow-xs">
{transI18n("actions")} {transI18n("actions")}
</label> </label>
<ul <ul
tabIndex={0} 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" 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"
> >
<li> {navTabs.map((tab) => {
<button onClick={() => actionMove('')}> const Icon = tab.icon;
{transI18n("characterInformation")} return (
<li key={tab.path}>
<button
onClick={() => actionMove(tab.path)}
className="flex items-center gap-2 py-2 px-3 text-xs font-semibold"
>
<Icon className="w-4 h-4" />
<span>{tab.label}</span>
</button> </button>
</li> </li>
<li> );
<button onClick={() => actionMove('relics-info')}> })}
{transI18n("relics")} <li className="border-t border-base-300 pt-1 mt-1">
</button> <button
</li> onClick={handleConnectOrSyncPS}
<li> className="btn btn-primary btn-sm w-full font-bold mt-1"
<button onClick={() => actionMove('eidolons-info')}> >
{transI18n("eidolons")} <RefreshCw className="w-3.5 h-3.5" />
</button> <span>{isConnectPS ? transI18n("sync") : transI18n("connectPs")}</span>
</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> </button>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
{/* Navigation Tabs Row (>= 640px) */}
<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">
<div className="flex sm:grid sm:grid-cols-5 w-full min-w-max gap-1">
{navTabs.map((tab) => {
const isActive = currentActivePath === tab.path;
const Icon = tab.icon;
return (
<button
key={tab.path}
onClick={() => actionMove(tab.path)}
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 ${
isActive
? "bg-primary text-primary-content shadow-xs"
: "text-base-content/75 hover:text-base-content hover:bg-base-100"
}`}
>
<Icon className="w-4 h-4 shrink-0" />
<span>{tab.label}</span>
</button>
);
})}
</div> </div>
</div> </div>
<div className="hidden sm:grid grid-cols-3 gap-2 w-full"> {/* Modals */}
<button className="btn btn-success btn-sm" onClick={() => actionMove("")}>
{transI18n("characterInformation")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("relics-info")}>
{transI18n("relics")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("eidolons-info")}>
{transI18n("eidolons")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("skills-info")}>
{transI18n("skills")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("showcase-card")}>
{transI18n("showcaseCard")}
</button>
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
</button>
</div>
{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 className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{title}
</h3>
</div> </div>
<div className="p-4 sm:p-6 overflow-y-auto max-h-[calc(90vh-5rem)]">
{content} {content}
</div> </div>
</div>
<form method="dialog" className="modal-backdrop">
<button onClick={onClose}>close</button>
</form>
</dialog> </dialog>
))} ))}
</div>
</div> </div>
); );
} }
+107 -48
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,88 +74,126 @@ 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="input input-bordered input-primary w-full" className="grow text-xs bg-transparent border-none outline-none focus:outline-none placeholder:text-base-content/50"
value={avatarSearch} value={avatarSearch}
onChange={(e) => setAvatarSearch(e.target.value)} onChange={(e) => setAvatarSearch(e.target.value)}
/> />
</div> {avatarSearch && (
<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"> <button
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => ( type="button"
<div 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} key={key}
type="button"
onClick={() => { onClick={() => {
setMapAvatarElementActive({ ...mapAvatarElementActive, [key]: !mapAvatarElementActive[key] }) setMapAvatarElementActive({ ...mapAvatarElementActive, [key]: !mapAvatarElementActive[key] })
}} }}
className="hover:bg-gray-600 grid items-center justify-items-center cursor-pointer rounded-md shadow-lg" className={`h-8.5 w-full rounded-lg transition-all duration-150 active-press flex items-center justify-center p-0 ${
style={{ isActive
backgroundColor: mapAvatarElementActive[key] ? "#374151" : "#6B7280" ? "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 <Image
src={`${process.env.CDN_URL}/${value.Icon}`} src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key} alt={key}
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md" className="w-6.5 h-6.5 object-contain drop-shadow-xs"
width={200} width={32}
height={200} /> height={32}
</div> />
))} </button>
);
})}
</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]"> {/* Path Filters - 9 Paths on 1 Row (gap-0.5) */}
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => ( <div className="grid grid-cols-9 gap-0.5">
<div {Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => {
const isActive = !!mapAvatarPathActive[key];
return (
<button
key={key} key={key}
type="button"
onClick={() => { onClick={() => {
setMapAvatarPathActive({ ...mapAvatarPathActive, [key]: !mapAvatarPathActive[key] }) setMapAvatarPathActive({ ...mapAvatarPathActive, [key]: !mapAvatarPathActive[key] })
}} }}
className="hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-lg cursor-pointer" className={`h-8 w-full rounded-lg transition-all duration-150 active-press flex items-center justify-center p-0 ${
style={{ isActive
backgroundColor: mapAvatarPathActive[key] ? "#374151" : "#6B7280" ? "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 <Image
src={`${process.env.CDN_URL}/${value.Icon}`} src={`${process.env.CDN_URL}/${value.Icon}`}
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
alt={key} alt={key}
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md" className={`w-5.5 h-5.5 object-contain ${
width={200} isActive ? "theme-path-icon-active" : "theme-path-icon"
height={200} /> }`}
</div> width={28}
))} height={28}
/>
</button>
);
})}
</div> </div>
</div> </div>
<div className="flex items-start h-full"> {/* Character Count & Grid List */}
<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"> <div className="flex items-center justify-between text-xs font-semibold text-base-content/80 px-1 py-0.5">
{listAvatar.map((item, index) => ( <span className="flex items-center gap-1.5">
<div key={index} onClick={() => { <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); setAvatarSelected(item);
setSkillIDSelected(null) setSkillIDSelected(null)
if (onClose) onClose() if (onClose) onClose()
}}> }}
>
<CharacterCard data={item} /> <CharacterCard data={item} />
</div> </div>
))} ))}
</ul> </ul>
</div> </div>
</div> </div>
</div>
</div>
</div>
) )
} }
+256 -310
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,57 +111,39 @@ 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")}
</span>
</h2> </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">
ID: #{avatarSelected.ID}
</span>
</div> </div>
<div className="space-y-6"> <div className="divide-y divide-base-content/10">
{/* Level Control */} {/* Level Control */}
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10"> <div className="py-4 space-y-2.5">
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2"> <div className="flex items-center justify-between">
<div className="w-2 h-6 bg-linear-to-b from-info to-info/50 rounded-full"></div> <span className="text-sm font-bold text-base-content tracking-tight flex items-center gap-1.5">
{transI18n("levelConfiguration")} <span className="w-1.5 h-1.5 rounded-full bg-primary" />
</h4> {transI18n("characterLevel")}
</span>
<div className="form-control"> <span className="text-xs font-mono font-bold px-2.5 py-1 rounded-md bg-primary/15 text-primary border border-primary/25">
<label className="label"> {avatars[avatarSelected?.ID.toString() || ""]?.level} / 80
<span className="label-text font-medium">{transI18n("characterLevel")}</span> </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>
<div className="mt-2"> <div className="flex items-center gap-3 pt-0.5">
<input <input
type="range" type="range"
min="1" min="1"
@@ -130,26 +153,37 @@ export default function AvatarInfo() {
const newLevel = Math.min(80, Math.max(1, parseInt(e.target.value) || 1)); 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) } }); setAvatars({ ...avatars, [avatarSelected?.ID.toString() || ""]: { ...avatars[avatarSelected?.ID.toString() || ""], level: newLevel, promotion: calcPromotion(newLevel) } });
}} }}
className="range range-info range-sm w-full" className="range range-primary range-sm flex-1"
/> />
</div> <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>
</div> </div>
{/* Energy Control */} {/* Ultimate Energy Control */}
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10"> <div className="py-4 space-y-2.5">
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2"> <div className="flex items-center justify-between">
<div className="w-2 h-6 bg-linear-to-b from-warning to-warning/50 rounded-full"></div> <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")} {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> </span>
</label> <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 <input
type="range" type="range"
min="0" min="0"
@@ -160,16 +194,10 @@ export default function AvatarInfo() {
const newSpValue = Math.min(avatars[avatarSelected?.ID.toString() || ""]?.sp_max, Math.max(0, parseInt(e.target.value) || 0)); 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 } }); setAvatars({ ...avatars, [avatarSelected?.ID.toString() || ""]: { ...avatars[avatarSelected?.ID.toString() || ""], sp_value: newSpValue } });
}} }}
className="range range-warning range-sm w-full" className="range range-warning range-sm flex-1"
/> />
<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 <button
className="btn btn-sm btn-outline btn-warning" className="btn btn-sm btn-warning btn-outline h-8 min-h-8 font-bold px-3 rounded-lg shadow-xs active-press"
onClick={() => { onClick={() => {
if (!avatars[avatarSelected?.ID.toString() || ""]?.can_change_sp) return if (!avatars[avatarSelected?.ID.toString() || ""]?.can_change_sp) return
const newSpValue = Math.ceil(avatars[avatarSelected?.ID.toString() || ""]?.sp_max / 2); const newSpValue = Math.ceil(avatars[avatarSelected?.ID.toString() || ""]?.sp_max / 2);
@@ -181,58 +209,41 @@ export default function AvatarInfo() {
</button> </button>
</div> </div>
</div> </div>
</div>
{/* Technique Toggle */} {/* Technique Toggle */}
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10"> <div className="py-4 flex items-center justify-between gap-4">
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2"> <div className="space-y-1">
<div className="w-2 h-6 bg-linear-to-b from-accent to-accent/50 rounded-full"></div> <span className="text-sm font-bold text-base-content tracking-tight flex items-center gap-1.5">
{transI18n("battleConfiguration")} <span className="w-1.5 h-1.5 rounded-full bg-secondary" />
</h4> {transI18n("useTechnique")}
</span>
<div className="form-control"> <p className="text-xs text-base-content/70 font-medium pl-3">{transI18n("techniqueNote")}</p>
<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> </div>
<input <input
type="checkbox" type="checkbox"
checked={avatars[avatarSelected?.ID.toString() || ""]?.techniques.length > 0 } checked={avatars[avatarSelected?.ID.toString() || ""]?.techniques.length > 0}
onChange={(e) => { onChange={(e) => {
if (!avatarSelected || avatarSelected?.MazeBuff?.length == 0) return if (!avatarSelected || avatarSelected?.MazeBuff?.length == 0) return
const techniques = e.target.checked ? avatarSelected?.MazeBuff : []; const techniques = e.target.checked ? avatarSelected?.MazeBuff : [];
const newAvatar = { ...avatars[avatarSelected?.ID.toString() || ""], techniques: techniques }; const newAvatar = { ...avatars[avatarSelected?.ID.toString() || ""], techniques: techniques };
setAvatar(newAvatar); setAvatar(newAvatar);
}} }}
className="toggle toggle-accent" className="toggle toggle-primary"
/> />
</label>
<div className="text-xs text-base-content/60 mt-1">
{transI18n("techniqueNote")}
</div>
</div>
</div> </div>
{/* Enhancement Selection */} {/* Enhancement Selection */}
{avatarSelected?.Enhanced && ( {avatarSelected?.Enhanced && (
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10"> <div className="py-4 space-y-2">
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2"> <div className="flex items-center justify-between">
<svg className="w-5 h-5 text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <span className="text-sm font-bold text-base-content tracking-tight flex items-center gap-1.5">
<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" /> <span className="w-1.5 h-1.5 rounded-full bg-accent" />
</svg>
{transI18n("enhancement")} {transI18n("enhancement")}
</h4> </span>
<span className="text-xs font-mono text-secondary font-bold">
<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")} {avatars[avatarSelected?.ID.toString() || ""]?.enhanced || transI18n("origin")}
</span> </span>
</label> </div>
<select <select
value={avatars[avatarSelected?.ID.toString() || ""]?.enhanced || ""} value={avatars[avatarSelected?.ID.toString() || ""]?.enhanced || ""}
onChange={(e) => { onChange={(e) => {
@@ -246,7 +257,7 @@ export default function AvatarInfo() {
setAvatar(newAvatar) setAvatar(newAvatar)
} }
}} }}
className="select select-bordered select-secondary" className="select select-sm select-bordered w-full rounded-xl bg-base-200/50"
> >
<option value="">{transI18n("origin")}</option> <option value="">{transI18n("origin")}</option>
{Object.keys(avatarSelected?.Enhanced || {}).map((key) => ( {Object.keys(avatarSelected?.Enhanced || {}).map((key) => (
@@ -255,169 +266,37 @@ export default function AvatarInfo() {
</option> </option>
))} ))}
</select> </select>
<div className="text-xs text-base-content/60 mt-1"> <p className="text-xs text-base-content/60 font-medium">{transI18n("enhancedNote")}</p>
{transI18n("enhancedNote")}
</div>
</div>
</div> </div>
)} )}
</div> </div>
</div> </div>
</div> </div>
</div> {/* Right Column: Lightcone Equipment */}
</div> <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="m-2 min-h-96"> <div>
<div className="container"> {/* Card Header */}
<div className="card bg-base-200 shadow-xl"> <div className="flex items-center justify-between pb-4 mb-2 border-b border-base-content/10">
<div className="card-body"> <h2 className="text-base sm:text-lg font-bold flex items-center gap-2.5">
{/* Header */} <div className="w-1.5 h-5 bg-linear-to-b from-secondary to-primary rounded-full"></div>
<div className="flex items-center justify-between mb-6"> <span className="bg-clip-text text-transparent bg-linear-to-r from-secondary to-primary font-bold tracking-tight">
<h2 className="card-title text-2xl font-bold flex items-center gap-2">
<div className="w-2 h-8 bg-linear-to-b from-primary to-secondary rounded-full"></div>
{transI18n("lightconeEquipment")} {transI18n("lightconeEquipment")}
</span>
</h2> </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> </div>
{lightcone && lightconeDetail ? ( {lightcone && lightconeDetail ? (
<div className="grid lg:grid-cols-3 gap-6"> <div className="space-y-4">
<div className="lg:col-span-3"> {/* Lightcone Hero Block */}
{/* Level & Rank Controls */} <div className="grid grid-cols-1 sm:grid-cols-12 gap-4 items-center">
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10"> {/* Artwork */}
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2"> <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">
<div className="w-2 h-6 bg-linear-to-b from-accent to-accent/50 rounded-full"></div>
{transI18n("lightconeSettings")}
</h4>
<div className="grid md:grid-cols-2 gap-6">
{/* Level Input */}
<div className="form-control">
<label className="label">
<span className="label-text font-medium">{transI18n("level")}</span>
<span className="label-text-alt text-primary font-mono">{lightcone.level}/80</span>
</label>
<div className="relative">
<input
type="number"
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="input input-bordered w-full pr-16 font-mono"
placeholder={transI18n("placeholderLevel")}
/>
<div
onClick={() => {
const newLightcone = { ...lightcone, level: 80, promotion: calcPromotion(80) }
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 <Image
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
@@ -425,38 +304,81 @@ export default function AvatarInfo() {
height={1260} height={1260}
priority priority
src={`${process.env.CDN_URL}/${lightconeDetail?.Image?.ImagePath}`} src={`${process.env.CDN_URL}/${lightconeDetail?.Image?.ImagePath}`}
className="w-full h-full rounded-lg object-cover shadow-lg" className="w-full h-full object-cover"
alt="Lightcone" alt="Lightcone"
/> />
</div> </div>
</div>
{/* Lightcone Info & Controls */} {/* Meta & Stats */}
<div className="lg:col-span-2 space-y-6"> <div className="sm:col-span-8 space-y-3">
{/* Basic Info */} <div>
{lightconeDetail && ( <h3 className="text-base sm:text-lg font-bold tracking-tight text-base-content">
<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 <ParseText
locale={locale} locale={locale}
text={getLocaleName(locale, lightconeDetail.Name)} text={getLocaleName(locale, lightconeDetail.Name)}
/> />
</h3> </h3>
<div className="badge badge-outline badge-lg"> <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())} {transI18n(lightconeDetail.BaseType.toLowerCase())}
</div> </span>
<div className="badge badge-outline badge-lg"> <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) + "⭐"} {calcRarity(lightconeDetail.Rarity)}★
</div> </span>
<div className="badge badge-outline badge-lg"> <span className="text-xs font-mono text-base-content/60 font-medium">#{lightcone.item_id}</span>
{"id: " + lightcone.item_id}
</div> </div>
</div> </div>
<div className="prose prose-sm max-w-none"> {/* 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>
{/* Skill Description */}
<div className="p-3.5 rounded-xl bg-base-200/50 border border-base-content/10">
<div <div
className="text-base-content/80 leading-relaxed" className="text-xs sm:text-sm text-base-content/90 leading-relaxed font-normal"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: replaceByParam( __html: replaceByParam(
getLocaleName(locale, lightconeDetail.Skills.Desc), getLocaleName(locale, lightconeDetail.Skills.Desc),
@@ -465,25 +387,9 @@ export default function AvatarInfo() {
}} }}
/> />
</div> </div>
</div>
)}
{/* Action Buttons */}
</div> <div className="flex items-center gap-3 pt-2">
</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 <button
onClick={() => { onClick={() => {
if (avatarSelected) { if (avatarSelected) {
@@ -491,11 +397,48 @@ export default function AvatarInfo() {
handleShow("action_detail_modal") handleShow("action_detail_modal")
} }
}} }}
className="btn btn-success btn-lg" className="btn btn-primary btn-sm flex-1 rounded-xl font-bold shadow-xs"
> >
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> {transI18n("changeLightcone")}
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" /> </button>
<button
onClick={() => {
handleUpdateLightcone(() => null)
}}
className="btn btn-error btn-outline hover:btn-error btn-sm rounded-xl font-bold px-3.5 gap-1.5 transition-all"
title={transI18n("removeLightcone")}
>
<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-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg> </svg>
<span className="text-xs">{transI18n("removeLightcone")}</span>
</button>
</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")} {transI18n("equipLightcone")}
</button> </button>
</div> </div>
@@ -503,27 +446,30 @@ export default function AvatarInfo() {
</div> </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>
<div className="p-4 sm:p-6 overflow-y-auto max-h-[calc(90vh-5rem)]">
<LightconeBar /> <LightconeBar />
</div> </div>
</div>
<form method="dialog" className="modal-backdrop">
<button onClick={() => handleCloseModal("action_detail_modal")}>close</button>
</form>
</dialog> </dialog>
</div> </div>
); );
} }
+65 -32
View File
@@ -6,31 +6,48 @@ 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"
}`} }`}
> >
<div className="relative w-full h-32 lg:h-26 xl:h-36">
<Image <Image
width={376} width={376}
height={512} height={512}
@@ -38,46 +55,62 @@ export default function CharacterCard({ data }: CharacterCardProps) {
crossOrigin="anonymous" crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${data.Image.AvatarIconPath}`} src={`${process.env.CDN_URL}/${data.Image.AvatarIconPath}`}
priority={true} priority={true}
className="rounded-md w-full h-full object-contain" className="w-full h-full object-contain transition-transform duration-300 ease-out group-hover:scale-105"
alt="ALT" alt={data.ID.toString()}
/> />
{/* 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={32} width={28}
height={32} height={28}
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType?.[data.DamageType].Icon}`} src={`${process.env.CDN_URL}/${damageType[data.DamageType].Icon}`}
className="absolute top-0 left-0 w-6 h-6 rounded-full" className="absolute top-1 left-1 w-6 h-6 object-contain drop-shadow-sm"
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>
{/* Name Label - Clear Full Name Display */}
<div className="w-full py-1.5 px-1 text-center flex items-center justify-center bg-base-100 min-h-11">
<ParseText <ParseText
locale={locale} locale={locale}
text={getNameChar(locale, transI18n, data)} text={getNameChar(locale, transI18n, data)}
className=" 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"
w-full px-0.5
my-1
text-center font-bold text-shadow-white
leading-tight
wrap-break-word
text-sm sm:text-base 2xl:text-lg
"
/> />
</div>
</li> </li>
); );
} }
+89 -57
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 <Image
src={`${process.env.CDN_URL}/${mapAvatar?.[character.avatar_id.toString()]?.Image?.AvatarIconPath}`} src={`${process.env.CDN_URL}/${avatar.Image?.AvatarIconPath}`}
alt={getLocaleName(locale, mapAvatar?.[character.avatar_id.toString()]?.Name)} alt={getLocaleName(locale, avatar.Name)}
width={376} width={376}
height={512} height={512}
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
className="w-full h-full object-contain" className="w-full h-full object-contain transition-transform duration-300 group-hover:scale-105"
/> />
{damageType?.[avatar.DamageType]?.Icon && (
<div className="absolute top-1.5 left-1.5 p-0.5 rounded-full bg-black/60 backdrop-blur-xs border border-white/10">
<Image <Image
width={48} width={24}
height={48} height={24}
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType?.[mapAvatar?.[character.avatar_id.toString()]?.DamageType || ""]?.Icon}`} src={`${process.env.CDN_URL}/${damageType[avatar.DamageType].Icon}`}
className="absolute top-0 left-0 w-10 h-10 rounded-full" className="w-5 h-5 object-contain"
alt={mapAvatar[character.avatar_id.toString()]?.DamageType.toLowerCase()} alt={avatar.DamageType.toLowerCase()}
/> />
</div>
)}
{baseType?.[avatar.BaseType]?.Icon && (
<div className="absolute top-1.5 right-1.5 p-0.5 rounded-full bg-black/60 backdrop-blur-xs border border-white/10">
<Image <Image
width={48} width={24}
height={48} height={24}
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType?.[mapAvatar?.[character.avatar_id.toString()]?.BaseType || ""]?.Icon}`} src={`${process.env.CDN_URL}/${baseType[avatar.BaseType].Icon}`}
className="absolute top-0 right-0 w-10 h-10 rounded-full" className="w-5 h-5 object-contain"
alt={mapAvatar[character.avatar_id.toString()]?.BaseType.toLowerCase()} alt={avatar.BaseType.toLowerCase()}
style={{
boxShadow: "inset 0 0 8px 4px #9CA3AF"
}}
/> />
</div> </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>
{/* 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">
<div className="text-lg font-bold">
<ParseText <ParseText
text={getLocaleName(locale, mapLightCone[character.lightcone.item_id.toString()]?.Name)} text={getLocaleName(locale, mapLightCone?.[character.lightcone.item_id.toString()]?.Name)}
locale={locale} locale={locale}
/> />
</div> </div>
<div className="text-base mb-1">Lv.{character.lightcone.level} S{character.lightcone.rank}</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>
<span className="text-[11px] font-mono font-bold text-primary">S{character.lightcone.rank}</span>
</div> </div>
</div> </div>
)} )}
+20 -18
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,23 +12,24 @@ 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"
}`} }`}
> >
<div className="relative w-full h-full">
<Image <Image
loading="lazy" loading="lazy"
src={`${process.env.CDN_URL}/${data?.Image?.ThumbnailPath}`} src={`${process.env.CDN_URL}/${data?.Image?.ThumbnailPath}`}
@@ -35,19 +37,19 @@ export default function LightconeCard({ data }: LightconeCardProps) {
crossOrigin="anonymous" crossOrigin="anonymous"
width={348} width={348}
height={408} height={408}
className="w-full h-full rounded-md object-cover" className="w-full h-full object-cover transition-transform duration-300 ease-out group-hover:scale-105"
alt="ALT" alt={data?.Name?.en || "Lightcone"}
/> />
</div> </div>
</div>
{/* Name Label - Uniform Fixed-Height Footer */}
<div className="w-full h-11 px-1 py-0.5 text-center flex items-center justify-center bg-base-100">
<ParseText <ParseText
locale={locale} locale={locale}
text={text} text={text}
className="mt-2 px-1 text-center text-sm sm:text-base font-bold leading-tight" className="font-bold text-xs leading-tight line-clamp-2 text-base-content group-hover:text-primary transition-colors text-center"
/> />
</div>
</li> </li>
); );
} }
+39 -28
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">
<div className="text-lg font-bold">
<ParseText <ParseText
text={getLocaleName(locale, mapLightCone[profile.lightcone.item_id.toString()]?.Name)} text={getLocaleName(locale, mapLightCone?.[profile.lightcone.item_id.toString()]?.Name)}
locale={locale} locale={locale}
/> />
</div> </div>
<div className="text-base mb-1">Lv.{profile.lightcone.level} S{profile.lightcone.rank}</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>
<span className="text-xs font-mono font-bold text-primary">S{profile.lightcone.rank}</span>
</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>
+64 -125
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,136 +10,83 @@ 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' },
'3': { icon: '/relics/BODY.png', name: 'Body' },
'4': { icon: '/relics/FOOT.png', name: 'Feet' },
'5': { icon: '/relics/NECK.png', name: 'Planar Sphere' },
'6': { icon: '/relics/OBJECT.png', name: 'Link Rope' },
};
const current = slotMap[slot];
if (!current) return null;
return (
<div className="flex items-center gap-1.5 justify-center mt-2.5">
<Image <Image
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
src="/relics/HEAD.png" src={current.icon}
alt="Head" alt={current.name}
width={20} width={18}
height={20} height={18}
className="bg-black/50 rounded-full" className="w-4 h-4 object-contain theme-stat-icon opacity-85"
/> />
<h2>Head</h2> <span className="text-xs font-bold text-base-content">{current.name}</span>
</div> </div>
); );
case '2': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/HAND.png"
alt="Hand"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<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
cursor-pointer transition-transform transition-all duration-200
${selectedRelicSlot === slot ? 'ring-5 ring-success scale-105' : 'ring-3 ring-primary'} ${isSelected ? 'ring-2 ring-primary/70 shadow-md ring-offset-2 ring-offset-base-100' : 'hover:border-primary/60'}
`} `}
> >
<span>
<Image <Image
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
@@ -146,51 +94,42 @@ export default function RelicCard({ slot, avatarId }: RelicCardProps) {
alt="Relic" alt="Relic"
width={124} width={124}
height={124} height={124}
className="w-14 h-14 object-contain" className="w-14 h-14 sm:w-16 sm:h-16 object-contain drop-shadow-md"
/> />
</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>
) )
} }
+31 -16
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,16 +13,27 @@ 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" <div
className={`relative w-full h-full rounded-xl overflow-hidden flex items-center justify-center bg-linear-to-b ${
is5Star
? "from-yellow-400 via-amber-500/85 to-amber-700/70"
: "from-purple-400 via-purple-600/80 to-purple-800/60"
}`} }`}
> >
<Image <Image
@@ -31,39 +43,42 @@ const { baseType, damageType } = useDetailDataStore()
crossOrigin="anonymous" crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${data.Image.ActionAvatarHeadIconPath}`} src={`${process.env.CDN_URL}/${data.Image.ActionAvatarHeadIconPath}`}
priority={true} priority={true}
className="w-full h-full object-contain" className="w-full h-full object-contain transition-transform duration-200 group-hover:scale-105"
alt="Avatar" 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"> {damageType?.[data.DamageType]?.Icon && (
<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}/${damageType?.[data.DamageType]?.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="Element" alt="Element"
/> />
</div> </div>
<div className="absolute top-0 right-0 w-5 h-5 bg-black/40 rounded-full flex items-center justify-center p-0.5"> )}
{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 <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}/${baseType[data.BaseType].Icon}`}
className="w-full h-full object-contain" className="w-full h-full object-contain"
alt="Path" alt="Path"
/> />
</div> </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" />
</svg>
</div> </div>
)} )}
</div> </div>
</div>
); );
}; };
+31 -36
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,45 +47,38 @@ 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-row items-center gap-2 md:gap-3"> <div className="flex flex-wrap items-center justify-between gap-2 pb-2.5 border-b border-base-content/10">
<div className={`badge ${getBadgeClass(change.type)} gap-1 md:gap-2 p-1`}> <div className="flex items-center gap-2">
<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"> </div>
<Calendar className="w-3 h-3 md:w-4 md:h-4" />
<span className="text-sm">{change.date}</span> <div className="flex items-center gap-1.5 text-xs text-base-content/50 font-mono">
<Calendar className="w-3.5 h-3.5" />
<span>{change.date}</span>
</div> </div>
</div> </div>
{/* Changes List */} {/* Changes List */}
<ul className="space-y-3"> <ul className="space-y-2 pt-1 text-xs sm:text-sm text-base-content/85">
{change.items.map((item, itemIndex) => ( {change.items.map((item, itemIndex) => (
<li key={itemIndex} className="flex items-start gap-3"> <li key={itemIndex} className="flex items-start gap-2.5">
<div className="mt-1"> <div className="mt-1 w-1.5 h-1.5 rounded-full bg-primary shrink-0" />
<Check className="w-5 h-5 text-success" /> <span className="leading-relaxed">{item}</span>
</div>
<span className="text-base-content">{item}</span>
</li> </li>
))} ))}
</ul> </ul>
</div> </div>
</div>
))} ))}
</div> </div>
</div> </div>
+75 -41
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,17 +23,37 @@ 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 className="block text-xs font-bold uppercase tracking-wider text-base-content/70">
{transI18n("connectionType")}
</label> </label>
<select <select
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1" className="select select-bordered w-full rounded-xl bg-base-100 border-base-content/10 focus:border-primary text-sm font-medium"
value={connectionType} value={connectionType}
onChange={(e) => { onChange={(e) => {
setIsConnectPS(false) setIsConnectPS(false)
@@ -45,25 +67,26 @@ export default function ConnectBar() {
</div> </div>
{connectionType === PSConnectType.Other && ( {connectionType === PSConnectType.Other && (
<div className="flex flex-col md:space-x-4 mb-6 gap-2"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-2">
<div className="form-control w-full mb-4 md:mb-0"> <div className="space-y-1">
<label className="label"> <label className="block text-xs font-semibold text-base-content/70">
<span className="label-text font-semibold text-purple-300">{transI18n("serverUrl")}</span> {transI18n("serverUrl")}
</label> </label>
<input <input
type="text" type="text"
placeholder={transI18n("placeholderServerUrl")} placeholder={transI18n("placeholderServerUrl")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1" 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} value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)} onChange={(e) => setServerUrl(e.target.value)}
/> />
</div> </div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label"> <div className="space-y-1">
<span className="label-text font-semibold text-purple-300">{transI18n("privateType")}</span> <label className="block text-xs font-semibold text-base-content/70">
{transI18n("privateType")}
</label> </label>
<select <select
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1" 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} value={privateType}
onChange={(e) => setPrivateType(e.target.value)} onChange={(e) => setPrivateType(e.target.value)}
> >
@@ -72,26 +95,27 @@ export default function ConnectBar() {
</select> </select>
</div> </div>
<div className="form-control w-full mb-4 md:mb-0"> <div className="space-y-1">
<label className="label"> <label className="block text-xs font-semibold text-base-content/70">
<span className="label-text font-semibold text-purple-300">{transI18n("username")}</span> {transI18n("username")}
</label> </label>
<input <input
type="text" type="text"
placeholder={transI18n("placeholderUsername")} placeholder={transI18n("placeholderUsername")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1" 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} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
/> />
</div> </div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label"> <div className="space-y-1">
<span className="label-text font-semibold text-purple-300">{transI18n("password")}</span> <label className="block text-xs font-semibold text-base-content/70">
{transI18n("password")}
</label> </label>
<input <input
type="password" type="password"
placeholder={transI18n("placeholderPassword")} placeholder={transI18n("placeholderPassword")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1" 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} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
/> />
@@ -99,27 +123,35 @@ export default function ConnectBar() {
</div> </div>
)} )}
{/* Status Message Notification */}
{message.text && ( {message.text && (
<div className={`alert ${message.type === 'success' ? 'alert-success' : <div className={`p-3 rounded-xl border text-xs flex items-center gap-2 ${
message.type === 'error' ? 'alert-error' : 'alert-info' message.type === 'success'
} mb-6`}> ? '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> <span>{message.text}</span>
</div> </div>
)} )}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 mb-2"> {/* Footer Controls */}
<div className="flex items-center justify-center md:justify-start"> <div className="flex flex-col sm:flex-row items-center justify-between gap-3 pt-3 border-t border-base-content/10">
<span className="text-md mr-2">{transI18n("status")}:</span> <div className="flex items-center gap-2">
<span <span className="text-xs text-base-content/60">{transI18n("status")}:</span>
className={`badge ${isConnectPS ? "badge-success" : "badge-error" <div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-mono font-bold border ${
} badge-lg`} isConnectPS
> ? "bg-emerald-500/15 border-emerald-500/30 text-emerald-400"
{isConnectPS ? transI18n("connected") : transI18n("unconnected")} : "bg-rose-500/15 border-rose-500/30 text-rose-400"
</span> }`}>
<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>
{/* Buttons */} <div className="flex items-center gap-2 w-full sm:w-auto">
<div className="flex flex-col sm:flex-row gap-2 w-full justify-center md:justify-end">
<button <button
onClick={async () => { onClick={async () => {
const response = await connectToPS(); const response = await connectToPS();
@@ -135,8 +167,9 @@ export default function ConnectBar() {
}); });
} }
}} }}
className="btn btn-primary w-full sm:w-auto" className="btn btn-sm btn-primary rounded-xl font-bold flex-1 sm:flex-none shadow-xs"
> >
<Server className="w-3.5 h-3.5" />
{transI18n("connectPs")} {transI18n("connectPs")}
</button> </button>
@@ -156,14 +189,15 @@ export default function ConnectBar() {
}); });
} }
}} }}
className="btn btn-success w-full sm:w-auto" 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")} {transI18n("sync")}
</button> </button>
)} )}
</div> </div>
</div> </div>
</div>
</div> </div>
) )
} }
+91 -34
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,32 +34,67 @@ export default function EidolonsInfo() {
return avatarSelected?.Ranks return avatarSelected?.Ranks
}, [avatarSelected, avatars]); }, [avatarSelected, avatars]);
return ( const currentRank = avatars[avatarSelected?.ID.toString() || ""]?.data?.rank || 0;
<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">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("eidolons")}
</h2>
<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">
{charRank && avatars[avatarSelected?.ID || ""] && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Object.entries(charRank || {}).map(([key, rank]) => (
<div key={key}
className="flex flex-col items-center"
> const handleToggleRank = (rankNum: number) => {
<div if (!avatarSelected) return;
className="cursor-pointer" let newRank = rankNum;
onClick={() => { if (currentRank === rankNum) {
let newRank = Number(rank.Rank) newRank = rankNum - 1;
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 } } }) setAvatars({
}} ...avatars,
[avatarSelected.ID]: {
...avatars[avatarSelected.ID],
data: {
...avatars[avatarSelected.ID]?.data,
rank: newRank,
},
},
});
};
return (
<div className="w-full">
<div className="bg-base-100 rounded-2xl p-4 sm:p-6 border border-base-300 shadow-sm space-y-6">
{/* Header */}
<div className="flex items-center justify-between pb-3 border-b border-base-300">
<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("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 || ""] && (
<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]) => {
const rankNum = Number(rank.Rank);
const isUnlocked = rankNum <= currentRank;
return (
<div
key={key}
className="flex flex-col items-center text-center space-y-3 p-2"
>
{/* 1. Organic Floating Eidolon Shard (No artificial rectangle wrapper) */}
<div
onClick={() => handleToggleRank(rankNum)}
className="cursor-pointer select-none transition-transform duration-200 hover:scale-105 active:scale-95"
title={`Click to ${isUnlocked ? 'deactivate' : 'activate'} E${rank.Rank}`}
> >
<Image <Image
className={`w-60 object-contain mb-2 ${Number(rank.Rank) <= avatars[avatarSelected?.ID.toString() || ""]?.data?.rank ? "" : "grayscale"}`} className={`w-44 sm:w-52 h-auto object-contain drop-shadow-md transition-all duration-300 ${
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`} src={`${process.env.CDN_URL}/ui/ui3d/rank/_dependencies/textures/${avatarSelected?.ID}/${avatarSelected?.ID}_rank_${rank.Rank}.png`}
alt={`Rank ${rank.Rank} Image`} alt={`Rank ${rank.Rank} Image`}
priority priority
@@ -59,24 +103,37 @@ export default function EidolonsInfo() {
width={240} width={240}
height={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">
<div
dangerouslySetInnerHTML={{
__html: replaceByParam(getLocaleName(locale, rank.Desc), rank.Param),
}}
/>
<ExtraEffectList extras={rank?.Extra} locale={locale} /> <ExtraEffectList extras={rank?.Extra} locale={locale} />
</div> </div>
</div> </div>
))} );
})}
</div> </div>
)} )}
</div> </div>
+194 -180
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,19 +66,25 @@ 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">
<h3 className="text-sm font-bold uppercase tracking-wider text-primary flex items-center gap-2">
<span className="w-1.5 h-3.5 bg-primary rounded-full"></span>
{transI18n("theoryCraft")} {transI18n("theoryCraft")}
<div className="tooltip tooltip-info tooltip-bottom" data-tip={transI18n("detailTheoryCraft")}>
<Info className="text-primary" size={20} />
</div>
</h3> </h3>
<div className="tooltip tooltip-left tooltip-info" data-tip={transI18n("detailTheoryCraft")}>
<Info className="text-primary w-4 h-4 cursor-pointer" />
</div>
</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" />
<span className="text-xs sm:text-sm font-semibold">{transI18n("theoryCraftMode")}</span>
</label>
<input <input
type="checkbox" type="checkbox"
className="toggle toggle-primary" className="toggle toggle-primary"
@@ -75,23 +99,21 @@ export default function ExtraSettingBar() {
stage_id: extraData?.theory_craft?.stage_id || 0 stage_id: extraData?.theory_craft?.stage_id || 0
} }
}) })
} }}
}
/> />
<span className="label-text font-semibold">{transI18n("theoryCraftMode")}</span> </div>
</label>
</motion.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" />
<span className="text-xs sm:text-sm font-semibold">{transI18n("cycleCount")}</span>
</div>
<input <input
type="number" type="number"
className="input input-primary" className="input input-sm input-bordered w-24 rounded-xl text-center font-mono font-bold"
value={extraData?.theory_craft?.cycle_count} value={extraData?.theory_craft?.cycle_count}
onChange={(e) => onChange={(e) =>
setExtraData({ setExtraData({
@@ -106,17 +128,17 @@ export default function ExtraSettingBar() {
} }
min="1" min="1"
/> />
</label> </div>
</motion.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" />
<span className="text-xs sm:text-sm font-semibold">{transI18n("stage")}</span>
</div>
<div className="relative"> <div className="relative">
<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-sm btn-outline w-full rounded-xl flex items-center justify-between text-xs sm:text-sm"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
if (!isChildClick) { if (!isChildClick) {
@@ -125,45 +147,29 @@ export default function ExtraSettingBar() {
setIsChildClick(false) setIsChildClick(false)
}} }}
> >
<Search className="w-6 h-6" /> <span className="truncate">
<span className="text-left"> {transI18n("stage")}: {getLocaleName(locale, stageList.find((s) => s.ID.toString() === extraData?.theory_craft?.stage_id?.toString())?.Name) || transI18n("selectStage")}</span> {getLocaleName(locale, stageList.find((s) => s.ID.toString() === extraData?.theory_craft?.stage_id?.toString())?.Name) || transI18n("selectStage")}
</span>
<Search className="w-3.5 h-3.5 shrink-0 ml-2" />
</button> </button>
</div>
{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 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
strokeLinejoin="round"
strokeLinecap="round"
strokeWidth="2.5"
fill="none"
stroke="currentColor"
>
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.3-4.3"></path>
</g>
</svg>
<input <input
type="search" className="grow" type="search"
className="input input-sm input-bordered w-full rounded-xl text-xs"
placeholder={transI18n("searchStage")} placeholder={transI18n("searchStage")}
value={stageSearchTerm} value={stageSearchTerm}
onChange={(e) => onChangeSearch(e.target.value)} onChange={(e) => onChangeSearch(e.target.value)}
autoFocus autoFocus
/> />
</label> <div className="max-h-56 overflow-y-auto space-y-1 pr-1 text-xs">
</div>
<div className="max-h-60 overflow-y-auto space-y-1">
{paginatedStages.length > 0 ? ( {paginatedStages.length > 0 ? (
<> paginatedStages.map((stage) => (
{paginatedStages.map((stage) => (
<div <div
key={stage.ID} key={stage.ID}
className="p-2 hover:bg-primary/20 rounded cursor-pointer" className="p-2 hover:bg-primary/20 rounded-xl cursor-pointer transition-colors"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
setIsChildClick(true) setIsChildClick(true)
@@ -186,19 +192,17 @@ export default function ExtraSettingBar() {
> >
{getLocaleName(locale, stage.Name)} {getLocaleName(locale, stage.Name)}
</div> </div>
))} ))
</>
) : ( ) : (
<div className="text-sm text-center py-4">{transI18n("noStageFound")}</div> <div className="text-xs text-center py-4 text-base-content/40">{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="flex items-center justify-between gap-2 pt-2 border-t border-white/5">
<button <button
disabled={stagePage === 1} disabled={stagePage === 1}
className="btn btn-sm btn-outline btn-success mt-2" className="btn btn-xs btn-outline rounded-lg"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
setStagePage(stagePage - 1) setStagePage(stagePage - 1)
@@ -206,10 +210,10 @@ export default function ExtraSettingBar() {
> >
{transI18n("previous")} {transI18n("previous")}
</button> </button>
<span className="text-[11px] font-mono text-base-content/50">Page {stagePage}</span>
<button <button
disabled={!hasMorePages} disabled={!hasMorePages}
className="btn btn-sm btn-outline btn-success mt-2" className="btn btn-xs btn-outline rounded-lg"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
setStagePage(stagePage + 1) setStagePage(stagePage + 1)
@@ -222,17 +226,17 @@ export default function ExtraSettingBar() {
</div> </div>
)} )}
</div> </div>
</label> </div>
</motion.div>
<motion.div className="form-control bg-base-200 p-4 rounded-xl shadow relative"> {/* Custom Lineup */}
<div className="flex flex-col gap-3"> <div className="p-3.5 rounded-xl bg-base-100/60 border border-white/5 space-y-2.5">
<label className="flex flex-wrap items-center justify-start gap-3"> <div className="flex items-center gap-2">
<UsersIcon className="text-info" size={20} /> <UsersIcon className="text-info w-4 h-4" />
<span className="label-text font-semibold">{transI18n("customLineup")}</span> <span className="text-xs sm:text-sm font-semibold">{transI18n("customLineup")}</span>
</label> </div>
<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 && ( <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) => { {(extraData?.theory_craft?.custom_lineup || []).map((avatarId) => {
const avatarData = mapAvatar[avatarId]; const avatarData = mapAvatar[avatarId];
if (!avatarData) return null; if (!avatarData) return null;
return ( return (
@@ -255,31 +259,30 @@ export default function ExtraSettingBar() {
}} }}
/> />
); );
}) })}
)}
<button <button
className="btn btn-outline btn-square border-dashed w-16 h-16 sm:w-20 sm:h-20" className="btn btn-outline btn-square border-dashed w-14 h-14 sm:w-16 sm:h-16 rounded-xl"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
setShowSearchLineup(pre => !pre); 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> </button>
</div> </div>
{showSearchLineup && ( {showSearchLineup && (
<div <div
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
className="mt-2 w-full z-50 border bg-base-200 border-slate-600 rounded-lg p-4 shadow-2xl" 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 mb-3"> <div className="flex justify-between items-center pb-2 border-b border-base-content/10">
<span className="font-semibold text-sm">{transI18n("selectedCharacters")}</span> <span className="font-bold text-xs">{transI18n("selectedCharacters")}</span>
<button className="btn btn-sm btn-circle btn-error" onClick={() => setShowSearchLineup(false)}>X</button> <button className="btn btn-xs btn-circle btn-ghost" onClick={() => setShowSearchLineup(false)}>✕</button>
</div> </div>
<div className="flex flex-wrap gap-2 max-h-72 overflow-y-auto p-1"> <div className="flex flex-wrap gap-2 max-h-60 overflow-y-auto p-1">
{Object.values(mapAvatar).map((avatar) => { {Object.values(mapAvatar).map((avatar) => {
const currentLineup = extraData?.theory_craft?.custom_lineup || []; const currentLineup = extraData?.theory_craft?.custom_lineup || [];
const isSelected = currentLineup.includes(avatar.ID.toString()); const isSelected = currentLineup.includes(avatar.ID.toString());
@@ -317,18 +320,21 @@ export default function ExtraSettingBar() {
</div> </div>
)} )}
</div> </div>
</motion.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">
<div className="flex items-center gap-2">
<h3 className="text-sm font-bold uppercase tracking-wider text-primary">
{transI18n("multipathCharacter")} {transI18n("multipathCharacter")}
<div className="tooltip tooltip-info tooltip-bottom" data-tip={transI18n("detailMultipathCharacter")}> </h3>
<Info className="text-primary" size={20} /> <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,18 +342,20 @@ 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>
</div>
<SelectCustomImage <SelectCustomImage
customSet={Object.values(mapAvatar).filter((it) => extraData?.multi_path?.multi_path_main?.includes(Number(it.ID))).map((it) => ({ customSet={Object.values(mapAvatar).filter((it) => extraData?.multi_path?.multi_path_main?.includes(Number(it.ID))).map((it) => ({
value: it.ID.toString(), value: it.ID.toString(),
label: getNameChar(locale, transI18n, it), label: getNameChar(locale, transI18n, it),
imageUrl: `${process.env.CDN_URL}/${baseType?.[it.BaseType].Icon}` imageUrl: `${process.env.CDN_URL}/${baseType?.[it.BaseType]?.Icon}`
}))} }))}
excludeSet={[]} excludeSet={[]}
selectedCustomSet={extraData?.multi_path?.main?.toString() || ""} selectedCustomSet={extraData?.multi_path?.main?.toString() || ""}
@@ -366,19 +374,18 @@ export default function ExtraSettingBar() {
}) })
}} }}
/> />
</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} />
<span className="label-text font-semibold">{transI18n("march7Path")}</span>
<SelectCustomImage <SelectCustomImage
customSet={Object.values(mapAvatar).filter((it) => extraData?.multi_path?.multi_path_march_7?.includes(Number(it.ID))).map((it) => ({ customSet={Object.values(mapAvatar).filter((it) => extraData?.multi_path?.multi_path_march_7?.includes(Number(it.ID))).map((it) => ({
value: it.ID.toString(), value: it.ID.toString(),
label: getNameChar(locale, transI18n, it), label: getNameChar(locale, transI18n, it),
imageUrl: `${process.env.CDN_URL}/${baseType?.[it.BaseType].Icon}` imageUrl: `${process.env.CDN_URL}/${baseType?.[it.BaseType]?.Icon}`
}))} }))}
excludeSet={[]} excludeSet={[]}
selectedCustomSet={extraData?.multi_path?.march_7?.toString() || ""} selectedCustomSet={extraData?.multi_path?.march_7?.toString() || ""}
@@ -397,26 +404,33 @@ export default function ExtraSettingBar() {
}) })
}} }}
/> />
</label> </div>
</motion.div> </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>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{extraData?.setting?.hide_ui !== undefined && ( {extraData?.setting?.hide_ui !== undefined && (
<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"> <div className="flex items-center gap-2.5">
{extraData?.setting?.hide_ui {extraData?.setting?.hide_ui
? <EyeOff className="text-warning" size={20} /> ? <EyeOff className="text-warning w-4 h-4" />
: <Eye className="text-success" size={20} /> : <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 className="tooltip tooltip-info tooltip-bottom" data-tip={transI18n("detailHiddenUi")}>
<Info className="text-primary" size={20} />
</div> </div>
</label>
</motion.div>
)} )}
{extraData?.setting?.censorship !== undefined && ( {extraData?.setting?.censorship !== undefined && (
<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"> <div className="flex items-center gap-2.5">
<ShieldBan className="text-error" size={20} /> <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>
<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 className="tooltip tooltip-info tooltip-bottom" data-tip={transI18n("detailDisableCensorship")}>
<Info className="text-primary" size={20} />
</div> </div>
</label>
</motion.div>
)} )}
</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 && (
<div className="rounded-2xl overflow-hidden border border-white/10 shadow-inner">
<Editor <Editor
value={extraData?.lua || ""} value={extraData?.lua || ""}
onValueChange={(code) => onValueChange={(code) =>
setExtraData( 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" textareaClassName="outline-none font-mono"
className="text-sm font-mono min-h-75" className="text-xs font-mono min-h-60 bg-[#161822] text-white"
style={{ style={{
background: "#1e1e1e", background: "#161822",
color: "#fff", color: "#e2e8f0",
}} }}
/> />
</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>
) )
} }
+139 -99
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,25 @@ 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">
<div className="navbar-start gap-2">
{/* Mobile menu dropdown */} {/* Mobile menu dropdown */}
<div className="dropdown"> <div className="dropdown">
<div tabIndex={0} role="button" className="btn btn-ghost btn-sm lg:hidden hover:bg-base-200 transition-all duration-300"> <div tabIndex={0} role="button" className="btn btn-ghost btn-sm lg:hidden rounded-lg hover:bg-base-200 transition-all duration-200">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h8m-8 6h16" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h8m-8 6h16" />
</svg> </svg>
</div> </div>
<ul <ul
tabIndex={0} tabIndex={0}
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-10 mt-3 w-52 p-2 shadow-md border border-base-200" 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> <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-2 hover:bg-base-200/70 rounded-lg transition-all duration-200 font-medium w-full">{transI18n("loadData")}</summary>
<ul className="p-2"> <ul className="p-2 space-y-1">
<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 +306,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 +322,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-2 hover:bg-base-200/70 rounded-lg transition-all duration-200 font-medium">{transI18n("exportData")}</summary>
<ul className="p-2"> <ul className="p-2 space-y-1">
<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 +335,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 +349,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-2 hover:bg-base-200/70 rounded-lg transition-all duration-200 font-medium"
onClick={() => { onClick={() => {
setIsOpenConnect(true) setIsOpenConnect(true)
}} }}
@@ -320,7 +362,7 @@ 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-2 hover:bg-base-200/70 rounded-lg transition-all duration-200 font-medium"
> >
{transI18n("monsterSetting")} {transI18n("monsterSetting")}
</button> </button>
@@ -332,7 +374,7 @@ export default function Header() {
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-2 hover:bg-base-200/70 rounded-lg transition-all duration-200 font-medium"
> >
{transI18n("extraSetting")} {transI18n("extraSetting")}
</button> </button>
@@ -341,10 +383,10 @@ export default function Header() {
</ul> </ul>
</div> </div>
{/* Logo */} {/* Brand Logo */}
<a className="flex items-center gap-2.5 px-2 group cursor-pointer select-none">
<a className="hidden sm:grid sm:grid-cols-1 items-start justify-items-center text-left gap-0 hover:scale-105 px-2"> <div className="relative">
<div className="flex items-center justify-center gap-2"> <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 <Image
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
@@ -352,30 +394,29 @@ export default function Header() {
alt="Logo" alt="Logo"
width={250} width={250}
height={250} height={250}
className="w-10 h-10 xl:w-12 xl:h-12 object-contain" 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"> <div className="flex flex-col justify-center items-start">
<h1 className="text-lg xl:text-xl font-bold leading-tight"> <h1 className="text-base xl:text-lg font-bold leading-tight tracking-tight flex items-center gap-1">
<span className="text-emerald-500">Firefly Sr</span> <span className="text-primary font-extrabold">Firefly</span>
<span className="bg-clip-text text-transparent bg-linear-to-r from-emerald-400 via-orange-500 to-red-500"> <span className="bg-clip-text text-transparent bg-linear-to-r from-emerald-400 via-amber-400 to-rose-400">
Tools SrTools
</span> </span>
</h1> </h1>
<p className="text-sm text-gray-500">Firefly Shelter</p> <p className="text-[11px] text-base-content/50 font-medium tracking-wide uppercase">Firefly Shelter</p>
</div>
</div> </div>
</a> </a>
</div> </div>
{/* Desktop navigation */} {/* Desktop navigation */}
<div className="navbar-center hidden lg:flex"> <div className="navbar-center hidden lg:flex">
<ul className="menu menu-horizontal gap-1"> <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">{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>
@@ -389,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>
@@ -404,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,
@@ -417,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,
@@ -431,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)
}} }}
@@ -444,7 +486,7 @@ export default function Header() {
onClick={() => { onClick={() => {
setIsOpenMonster(true) setIsOpenMonster(true)
}} }}
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"
> >
{transI18n("monsterSetting")} {transI18n("monsterSetting")}
</button> </button>
@@ -455,7 +497,7 @@ export default function Header() {
onClick={() => { onClick={() => {
setIsOpenExtra(true) setIsOpenExtra(true)
}} }}
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"
> >
{transI18n("extraSetting")} {transI18n("extraSetting")}
</button> </button>
@@ -465,53 +507,55 @@ export default function Header() {
</div> </div>
{/* Right side items */} {/* Right side items */}
<div className="navbar-end gap-2 pr-1 "> <div className="navbar-end gap-2 pr-1">
<div data-tip="Connection Status" className="tooltip tooltip-bottom"> {/* PS Connection Status Badge */}
<div className="flex items-center space-x-2 p-1.5 rounded-full shadow-md"> <div data-tip={isConnectPS ? "Connected to PS" : "Not Connected"} className="tooltip tooltip-bottom">
<div className={`hidden xl:block text-sm italic ${isConnectPS ? 'text-green-500' : 'text-red-500'}`}> <div className="flex items-center gap-2 px-2.5 py-1 rounded-full bg-base-200/60 border border-white/5 shadow-xs">
<span className="relative flex h-2 w-2">
{isConnectPS && (
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
)}
<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>
</span>
<span className={`hidden xl:block text-xs font-semibold tracking-tight ${isConnectPS ? 'text-emerald-500' : 'text-base-content/50'}`}>
{isConnectPS ? transI18n("connected") : transI18n("unconnected")} {isConnectPS ? transI18n("connected") : transI18n("unconnected")}
</div> </span>
<div className={`w-3 h-3 rounded-full ${isConnectPS ? 'bg-green-500' : 'bg-red-500'}`}></div>
</div> </div>
</div> </div>
<div {/* View Logs Button */}
data-tip="View Logs" <button
className="btn btn-ghost btn-sm btn-circle items-center justify-center w-fit tooltip tooltip-bottom" 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={() => { onClick={() => {
setIsChangelog(true) setIsChangelog(true)
}} }}
> >
<ScrollText className="w-5 h-5 text-warning" /> <ScrollText className="w-4 h-4" />
</div> </button>
{/* Language selector - REFINED */} {/* Language selector */}
<div className="dropdown dropdown-end"> <div className="dropdown dropdown-end tooltip tooltip-bottom" data-tip={transI18n("language") || "Language"}>
{/* Nút bấm hiển thị */}
<div <div
tabIndex={0} tabIndex={0}
role="button" 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" 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"
> >
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-4"> <span className="font-bold">{listCurrentLanguage[locale as keyof typeof listCurrentLanguage]?.flag || "EN"}</span>
<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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="size-3.5 opacity-60">
</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" /> <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> </svg>
</div> </div>
<ul <ul
tabIndex={0} tabIndex={0}
className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box w-52 mt-2" 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"
> >
{Object.entries(listCurrentLanguage).map(([key, value]) => ( {Object.entries(listCurrentLanguage).map(([key, value]) => (
<li key={key}> <li key={key}>
<button <button
className={`flex justify-between ${locale === key ? "active" : ""}`} 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"}`}
onClick={() => { onClick={() => {
changeLocale(key); changeLocale(key);
const elem = document.activeElement; const elem = document.activeElement;
@@ -527,8 +571,10 @@ export default function Header() {
))} ))}
</ul> </ul>
</div> </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"> {/* 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 <svg
width={16} width={16}
height={16} height={16}
@@ -544,22 +590,13 @@ export default function Header() {
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" 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>
<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>
<div <div
tabIndex={0} 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" 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-40 p-1"> <ul className="menu w-36 p-1.5 space-y-0.5">
{themes.map((theme) => ( {themes.map((theme) => (
<li <li
key={theme.label} key={theme.label}
@@ -568,20 +605,19 @@ export default function Header() {
}} }}
> >
<button <button
className="gap-2 px-2 py-1.5 hover:bg-base-200 rounded text-sm transition-all duration-200" 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()} data-set-theme={theme.label.toLowerCase()}
data-act-class="[&_svg]:visible"
> >
<div <div
data-theme={theme.label.toLowerCase()} data-theme={theme.label.toLowerCase()}
className="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded p-1 shadow-sm" 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-base-content size-1 rounded-full" />
<div className="bg-primary size-1 rounded-full" /> <div className="bg-primary size-1 rounded-full" />
<div className="bg-secondary size-1 rounded-full" /> <div className="bg-secondary size-1 rounded-full" />
<div className="bg-accent size-1 rounded-full" /> <div className="bg-accent size-1 rounded-full" />
</div> </div>
<div className="text-sm">{theme.label}</div> <span>{theme.label}</span>
</button> </button>
</li> </li>
))} ))}
@@ -589,31 +625,35 @@ export default function Header() {
</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 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 className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{title}
</h3>
</div> </div>
{/* Modal Body */}
<div className="p-4 sm:p-6 overflow-y-auto flex-1">
{content} {content}
</div> </div>
</div>
<form method="dialog" className="modal-backdrop">
<button onClick={onClose}>close</button>
</form>
</dialog> </dialog>
))} ))}
</div> </header>
) )
} }
+147 -90
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,96 +181,108 @@ 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">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key} key={key}
onClick={() => { type="button"
setListPath({ ...listPath, [key]: !listPath[key] }) onClick={() => setListPath({ ...listPath, [key]: !listPath[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 ${
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer" isActive
style={{ ? "bg-primary/20 border-primary ring-2 ring-primary/50 shadow-sm scale-105"
backgroundColor: listPath[key] ? "#374151" : "#6B7280" : "bg-base-200 hover:bg-base-300 border-base-300 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-8 w-8 object-contain rounded-md" className={`w-6 h-6 sm:w-7 sm:h-7 object-contain ${
width={200} isActive ? "theme-path-icon-active" : "theme-path-icon"
height={200} }`}
width={32}
height={32}
/> />
</div> </button>
))} );
</div> })}
</div> </div>
{/* Element */} <div className="h-8 w-px bg-base-300 hidden md:block" />
<div>
<div className="flex flex-wrap gap-2 justify-start items-center"> {/* Element Filters */}
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => ( <div className="flex flex-wrap items-center gap-1.5 sm:gap-2">
<div {Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => {
const isActive = !!listElement[key];
return (
<button
key={key} key={key}
onClick={() => { type="button"
setListElement({ ...listElement, [key]: !listElement[key] }) onClick={() => setListElement({ ...listElement, [key]: !listElement[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 ${
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer" isActive
style={{ ? "bg-primary/20 border-primary ring-2 ring-primary/50 shadow-sm scale-105"
backgroundColor: listElement[key] ? "#374151" : "#6B7280" : "bg-base-200 hover:bg-base-300 border-base-300 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 2xl:h-10 2xl:w-10 object-contain rounded-md" className="w-6 h-6 sm:w-7 sm:h-7 object-contain drop-shadow-xs"
width={200} width={32}
height={200} height={32}
/> />
</div> </button>
))} );
</div> })}
</div> </div>
{/* Rank */} <div className="h-8 w-px bg-base-300 hidden md:block" />
<div>
<div className="flex flex-wrap gap-2 justify-end items-center"> {/* Rank Filters */}
{Object.entries(listRank).map(([key], index) => ( <div className="flex items-center gap-1.5 sm:gap-2">
<div {Object.entries(listRank).map(([key], index) => {
const isActive = !!listRank[key];
return (
<button
key={index} key={index}
onClick={() => { type="button"
setListRank({ ...listRank, [key]: !listRank[key] }) 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 ${
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer" isActive
style={{ ? "bg-primary text-primary-content border-primary shadow-sm scale-105"
backgroundColor: listRank[key] ? "#374151" : "#6B7280" : "bg-base-200 hover:bg-base-300 text-base-content/70 border-base-300"
}}> }`}
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">{key}*</div> >
</div> {key}★
))} </button>
);
})}
</div> </div>
</div> </div>
</div> </div>
</div> {/* Character Select Dropdown */}
<div className="grid grid-cols-1 gap-2">
{listAvatar.length > 0 && ( {listAvatar.length > 0 && (
<div> <div className="space-y-1.5 pt-2 border-t border-base-300/60">
<div>{transI18n("characterName")}</div> <div className="text-xs font-bold text-base-content/70">{transI18n("characterName")}</div>
<SelectCustomImage <SelectCustomImage
customSet={listAvatar.map((avatar) => ({ customSet={listAvatar.map((avatar) => ({
value: avatar.ID.toString(), value: avatar.ID.toString(),
@@ -252,41 +296,54 @@ export default function CopyImport() {
/> />
</div> </div>
)} )}
</div>
</div> </div>
{/* Selection Action Bar */}
<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">
{/* Selection Info */} <div className="flex items-center gap-2">
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg"> <span className="text-xs sm:text-sm font-bold text-base-content">
<span className="text-blue-400 font-medium"> {transI18n("selectedProfiles")}:
{transI18n("selectedProfiles")}: {selectedProfiles.length}
</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) => {
+70 -33
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,83 +138,97 @@ 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 = { if (newProfile) {
newProfileList.push(newProfile)
}
const newAvatar = {
...currentAvatar,
level: character.level ?? 0,
promotion: character.promotion ?? 0,
data: {
rank: character.rank ?? 0, rank: character.rank ?? 0,
skills: character.skillTreeList.reduce((acc, skill) => { skills: character.skillTreeList.reduce((acc, skill) => {
acc[skill.pointId] = skill.level; acc[skill.pointId] = skill.level;
return acc; return acc;
}, {} as Record<string, number>), }, {} as Record<string, number>),
} },
const newProfile = converterOneEnkaDataToAvatarStore(character, newAvatar.profileList.length) profileList: newProfileList,
if (newProfile) { profileSelect: newProfile ? newProfileList.length - 1 : currentAvatar.profileSelect,
newAvatar.profileList.push(newProfile)
newAvatar.profileSelect = newAvatar.profileList.length - 1
} }
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">
{transI18n("selectedCharacters")}:
</span> </span>
<span className="badge badge-primary font-mono font-bold text-xs">
{selectedCharacters.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 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
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")} {transI18n("selectAll")}
</button> </button>
</div> </div>
</div> </div>
</div>
{isLoading && ( {isLoading && (
<div className="flex justify-center items-center h-64"> <div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
+65 -31
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,68 +163,81 @@ 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 = { if (newProfile) {
newProfileList.push(newProfile)
}
const newAvatar = {
...currentAvatar,
level: character.level,
promotion: character.promotion,
data: {
rank: character.data.rank ?? 0, rank: character.data.rank ?? 0,
skills: character.data.skills skills: character.data.skills
} },
const newProfile = converterOneFreeSRDataToAvatarStore(parsed.data, newAvatar.profileList.length, character.avatar_id) profileList: newProfileList,
if (newProfile) { profileSelect: newProfile ? newProfileList.length - 1 : currentAvatar.profileSelect,
newAvatar.profileList.push(newProfile)
newAvatar.profileSelect = newAvatar.profileList.length - 1
} }
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">
{transI18n("selectedCharacters")}:
</span> </span>
<span className="badge badge-primary font-mono font-bold text-xs">
{selectedCharacters.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 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
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")} {transI18n("selectAll")}
</button> </button>
</div> </div>
</div> </div>
</div>
{isLoading && ( {isLoading && (
<div className="flex justify-center items-center h-64"> <div className="flex justify-center items-center h-64">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
+125 -58
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> {/* Results Grid */}
</div> <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">
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-8 mt-2 gap-2"> {listLightcone.map((item) => (
{listLightcone.map((item, index) => ( <div
<div key={index} onClick={() => { key={item.ID}
className="h-full flex flex-col"
onClick={() => {
if (avatarSelected) { if (avatarSelected) {
const avatar = avatars[avatarSelected?.ID?.toString()] const avatar = avatars[avatarSelected.ID.toString()]
avatar.profileList[avatar.profileSelect].lightcone = { if (avatar) {
const profileIndex = avatar.profileSelect
const currentProfile = avatar.profileList?.[profileIndex]
if (currentProfile) {
const newProfileList = [...avatar.profileList]
newProfileList[profileIndex] = {
...currentProfile,
lightcone: {
level: 80, level: 80,
item_id: item.ID, item_id: item.ID,
rank: 1, rank: 1,
promotion: 6 promotion: 6
} }
setAvatar({ ...avatar }) }
setAvatar({
...avatar,
profileList: newProfileList
})
setIsOpenLightcone(false) 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) => (
+60 -54
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,24 +155,25 @@ 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="p-2 hover:bg-primary/20 rounded cursor-pointer" className="btn btn-sm btn-outline justify-start"
onClick={() => { onClick={(e) => {
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 })
} }
@@ -172,21 +181,18 @@ export default function CeBar() {
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,21 +522,18 @@ export default function CeBar() {
))} ))}
{/* Add New Wave Button */} {/* Add New Wave Button */}
<div className="card border border-slate-700/50 border-dashed">
<div className="card-body p-8">
<button <button
onClick={() => { onClick={() => {
const newCeConfig = structuredClone(ce_config) const newCeConfig = structuredClone(ce_config)
newCeConfig.monsters.push([]) newCeConfig.monsters.push([])
setCeConfig(newCeConfig) setCeConfig(newCeConfig)
}} }}
className="btn btn-primary btn-lg w-full"> className="btn btn-outline btn-warning w-full flex items-center justify-center gap-2 rounded-xl shadow-xs font-semibold"
<Plus className="w-6 h-6 mr-2" /> >
{transI18n("addNewWave")} <Plus className="w-4 h-4" />
<span>{transI18n("addNewWave")}</span>
</button> </button>
</div> </div>
</div> </div>
</div>
</div>
); );
} }
+26 -10
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,19 +49,27 @@ 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) => {
const isSelected = battle_type.toUpperCase() === item.value.toUpperCase()
return (
<button <button
key={item.name} key={item.name}
onClick={() => setBattleType(item.value.toUpperCase())} onClick={() => setBattleType(item.value.toUpperCase())}
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() 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 ${
? 'bg-success/30 shadow-lg' isSelected
: 'bg-base-200/20 hover:bg-base-200/40 ' ? '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 <span
className={
isSelected
? ""
: "theme-path-icon"
}
style={ style={
battle_type.toUpperCase() === item.value.toUpperCase() isSelected
? { ? {
filter: filter:
'brightness(0) saturate(100%) invert(63%) sepia(78%) saturate(643%) hue-rotate(1deg) brightness(93%) contrast(89%)', 'brightness(0) saturate(100%) invert(63%) sepia(78%) saturate(643%) hue-rotate(1deg) brightness(93%) contrast(89%)',
@@ -70,11 +84,13 @@ export default function MonsterBar() {
alt={item.name} alt={item.name}
width={24} width={24}
height={24} height={24}
className="w-5 h-5 object-contain"
/> />
</span> </span>
<span>{item.name}</span> <span>{item.name}</span>
</button> </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) => (
+202 -110
View File
@@ -1,88 +1,119 @@
"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(
listFloor?.find((peak) => peak?.ID === peak_config.challenge_id),
);
if ( if (
challenge challenge &&
&& challenge.ID === mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID challenge.ID ===
&& bossConfig mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID &&
&& peak_config?.boss_mode === "Hard" bossConfig &&
peak_config?.boss_mode === "Hard"
) { ) {
return { challenge: bossConfig, isBoss: true } return { challenge: bossConfig, isBoss: true };
} }
return { return {
challenge: challenge, challenge: challenge,
isBoss: challenge?.ID === mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID, isBoss:
} challenge?.ID ===
}, [peak_config, listFloor, mapPeak, bossConfig]) mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID,
};
}, [peak_config, listFloor, mapPeak, bossConfig]);
useEffect(() => { useEffect(() => {
if (!challengeSelected.challenge) return if (!challengeSelected.challenge) return;
if (peak_config.event_id !== 0 && peak_config.challenge_id !== 0 && challengeSelected) { if (
const newBattleConfig = structuredClone(peak_config) peak_config.event_id !== 0 &&
newBattleConfig.cycle_count = 6 peak_config.challenge_id !== 0 &&
newBattleConfig.blessings = [] challengeSelected
) {
const newBattleConfig = structuredClone(peak_config);
newBattleConfig.cycle_count = 6;
newBattleConfig.blessings = [];
for (const value of challengeSelected?.challenge?.MazeBuff) { for (const value of challengeSelected?.challenge?.MazeBuff) {
newBattleConfig.blessings.push({ newBattleConfig.blessings.push({
id: value.ID, id: value.ID,
level: 1 level: 1,
}) });
} }
if (peak_config.buff_id !== 0 && challengeSelected.isBoss) { if (peak_config.buff_id !== 0 && challengeSelected.isBoss) {
newBattleConfig.blessings.push({ newBattleConfig.blessings.push({
id: peak_config.buff_id, id: peak_config.buff_id,
level: 1 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,
});
} }
newBattleConfig.monsters = []
newBattleConfig.stage_id = challengeSelected.challenge.EventList[0].ID
for (const wave of challengeSelected.challenge.EventList[0].MonsterList) { for (const wave of challengeSelected.challenge.EventList[0].MonsterList) {
if (!wave) continue if (!wave) continue;
const newWave: MonsterStore[] = [] const newWave: MonsterStore[] = [];
for (const value of Object.values(wave)) { for (const value of Object.values(wave)) {
if (!value) continue if (!value) continue;
newWave.push({ newWave.push({
monster_id: value, monster_id: value,
level: challengeSelected.challenge.EventList[0].Level, level: challengeSelected.challenge.EventList[0].Level,
amount: 1, amount: 1,
}) });
} }
newBattleConfig.monsters.push(newWave) newBattleConfig.monsters.push(newWave);
} }
setPeakConfig(newBattleConfig) setPeakConfig(newBattleConfig);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [
@@ -91,119 +122,145 @@ export default function PeakBar() {
peak_config.buff_id, peak_config.buff_id,
peak_config.boss_mode, peak_config.boss_mode,
mapPeak, mapPeak,
]) ]);
if (!mapPeak) return null if (!mapPeak) 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(mapPeak).sort((a, b) => b.ID - a.ID).map((peak) => ({ customSet={Object.values(mapPeak)
.sort((a, b) => b.ID - a.ID)
.map((peak) => ({
id: peak.ID.toString(), id: peak.ID.toString(),
name: `${getLocaleName(locale, peak.Name)} (${peak.ID})`, name: `${getLocaleName(locale, peak.Name)} (${peak.ID})`,
}))} }))}
excludeSet={[]} excludeSet={[]}
selectedCustomSet={peak_config.event_id.toString()} selectedCustomSet={peak_config.event_id.toString()}
placeholder={transI18n("selectPEAKEvent")} placeholder={transI18n("selectPEAKEvent")}
setSelectedCustomSet={(id) => setPeakConfig({ ...peak_config, event_id: Number(id), challenge_id: 0, buff_id: 0 })} setSelectedCustomSet={(id) =>
setPeakConfig({
...peak_config,
event_id: Number(id),
challenge_id: 0,
buff_id: 0,
})
}
/> />
</div> </div>
{/* Settings */} {/* Settings */}
<div className={ <div
`grid grid-cols-1 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"> <div className="flex items-center gap-2 w-full">
<label className="label"> <label className="label">
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span> <span className="label-text font-bold text-success">
{transI18n("floor")}:{" "}
</span>
</label> </label>
<select <select
value={peak_config.challenge_id} value={peak_config.challenge_id}
className="select select-success w-full" className="select select-success w-full"
onChange={(e) => setPeakConfig({ ...peak_config, challenge_id: Number(e.target.value) })} onChange={(e) =>
setPeakConfig({
...peak_config,
challenge_id: Number(e.target.value),
})
}
> >
<option value={0} disabled={true}>{transI18n("selectFloor")}</option> <option value={0} disabled={true}>
{transI18n("selectFloor")}
</option>
{listFloor.map((peak) => ( {listFloor.map((peak) => (
<option key={peak.ID} value={peak.ID}>{getLocaleName(locale, peak.Name)}</option> <option key={peak.ID} value={peak.ID}>
{getLocaleName(locale, peak.Name)}
</option>
))} ))}
</select> </select>
</div> </div>
{eventSelected && eventSelected.BossLevel?.ID === peak_config.challenge_id && ( {eventSelected &&
eventSelected.BossLevel?.ID === peak_config.challenge_id && (
<div className="flex items-center gap-2 w-full"> <div className="flex items-center gap-2 w-full">
<label className="label"> <label className="label">
<span className="label-text font-bold text-success">{transI18n("mode")}:{" "}</span> <span className="label-text font-bold text-success">
{transI18n("mode")}:{" "}
</span>
</label> </label>
<select <select
value={peak_config.boss_mode} value={peak_config.boss_mode}
className="select select-success w-full" className="select select-success w-full"
onChange={(e) => setPeakConfig({ ...peak_config, boss_mode: e.target.value })} onChange={(e) =>
setPeakConfig({ ...peak_config, boss_mode: e.target.value })
}
> >
<option value={0} disabled={true}>{transI18n("selectSide")}</option> <option value={0} disabled={true}>
{transI18n("selectSide")}
</option>
<option value="Normal">{transI18n("normalMode")}</option> <option value="Normal">{transI18n("normalMode")}</option>
<option value="Hard">{transI18n("hardMode")}</option> <option value="Hard">{transI18n("hardMode")}</option>
</select> </select>
</div> </div>
)} )}
</div> </div>
<div className="label-text font-bold text-success mb-2">StageId: {peak_config?.stage_id}</div> <div className="label-text font-bold text-success mb-2">
{ StageId: {peak_config?.stage_id}
eventSelected </div>
&& eventSelected.BossLevel?.ID === peak_config.challenge_id {eventSelected &&
&& bossConfig eventSelected.BossLevel?.ID === peak_config.challenge_id &&
&& ( bossConfig && (
<div className="mb-4 w-full"> <div className="mb-4 w-full">
<SelectCustomText <SelectCustomText
customSet={ customSet={bossConfig.BuffList.map((buff) => ({
bossConfig.BuffList.map((buff) => ({
id: buff.ID.toString(), id: buff.ID.toString(),
name: getLocaleName(locale, buff?.Name || ""), name: getLocaleName(locale, buff?.Name || ""),
description: replaceByParam(getLocaleName(locale, buff?.Desc || ""), buff?.Param || []), description: replaceByParam(
})) getLocaleName(locale, buff?.Desc || ""),
} buff?.Param || [],
),
}))}
excludeSet={[]} excludeSet={[]}
selectedCustomSet={peak_config?.buff_id?.toString()} selectedCustomSet={peak_config?.buff_id?.toString()}
placeholder={transI18n("selectBuff")} placeholder={transI18n("selectBuff")}
setSelectedCustomSet={(id) => setPeakConfig({ ...peak_config, buff_id: Number(id) })} setSelectedCustomSet={(id) =>
setPeakConfig({ ...peak_config, buff_id: Number(id) })
}
/> />
</div> </div>
) )}
}
{/* Turbulence Buff */} {/* Turbulence Buff */}
<div className="pt-3 border-t border-base-content/10 space-y-2">
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20"> <h2 className="text-xl font-bold text-info">
<h2 className="text-2xl font-bold mb-2 text-info">
{transI18n("turbulenceBuff")} {transI18n("turbulenceBuff")}
</h2> </h2>
{challengeSelected?.challenge && challengeSelected?.challenge?.MazeBuff?.length > 0 ? ( {challengeSelected?.challenge &&
challengeSelected?.challenge?.MazeBuff?.length > 0 ? (
challengeSelected.challenge.MazeBuff.map((subOption, index) => ( challengeSelected.challenge.MazeBuff.map((subOption, index) => (
<div key={index}> <div key={index} className="space-y-1">
<label className="label"> <span className="font-bold text-success text-sm">
<span className="label-text font-bold text-success">
{index + 1}. {getLocaleName(locale, subOption.Name)} {index + 1}. {getLocaleName(locale, subOption.Name)}
</span> </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),
subOption.Param || [] subOption.Param || [],
) ),
}} }}
/> />
</div> </div>
)) ))
) : ( ) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div> <div className="text-sm text-base-content/50 italic">
{transI18n("noTurbulenceBuff")}
</div>
)} )}
</div> </div>
</div> </div>
@@ -212,23 +269,31 @@ export default function PeakBar() {
{(peak_config?.challenge_id ?? 0) !== 0 && ( {(peak_config?.challenge_id ?? 0) !== 0 && (
<div className="grid grid-cols-1 gap-4"> <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>
<div className="rounded-xl p-4 mt-2 border border-warning"> {challengeSelected?.challenge &&
<h2 className="text-2xl font-bold mb-2 text-info">{getLocaleName(locale, challengeSelected?.challenge?.Name)}</h2> Object.values(
challengeSelected?.challenge?.EventList?.[0]?.Infinite || [],
).map((waveValue, waveIndex) => (
{challengeSelected?.challenge && Object.values(challengeSelected?.challenge?.EventList?.[0]?.Infinite || []).map((waveValue, waveIndex) => (
<div key={waveIndex} className="mb-6"> <div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold">{transI18n("wave")} {waveIndex + 1}</h3> <h3 className="text-lg font-semibold">
{transI18n("wave")} {waveIndex + 1}
</h3>
<div className="flex flex-wrap gap-2 mt-2"> <div className="flex flex-wrap gap-2 mt-2">
{Array.from(new Set(waveValue.MonsterList)).map((monsterId, enemyIndex) => { {Array.from(new Set(waveValue.MonsterList)).map(
(monsterId, enemyIndex) => {
const monsterStats = calcMonsterStats( const monsterStats = calcMonsterStats(
mapMonster?.[monsterId.toString()], mapMonster?.[monsterId.toString()],
waveValue.EliteGroup, waveValue.EliteGroup,
challengeSelected?.challenge?.EventList?.[0]?.HardLevelGroup || 0, challengeSelected?.challenge?.EventList?.[0]
challengeSelected?.challenge?.EventList?.[0]?.Level || 0, ?.HardLevelGroup || 0,
challengeSelected?.challenge?.EventList?.[0]?.Level ||
0,
hardLevelConfig, hardLevelConfig,
eliteConfig eliteConfig,
); );
return ( return (
<div <div
@@ -236,11 +301,16 @@ export default function PeakBar() {
className="group relative flex flex-col w-40 bg-base-100 rounded-2xl border border-base-300 shadow-md" 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"> <div className="badge badge-warning badge-sm font-bold absolute top-2 right-2 z-10 shadow-sm">
Lv. {challengeSelected?.challenge?.EventList?.[0]?.Level} Lv.{" "}
{
challengeSelected?.challenge?.EventList?.[0]
?.Level
}
</div> </div>
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl"> <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 && ( {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"> <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 <Image
unoptimized unoptimized
@@ -258,18 +328,38 @@ export default function PeakBar() {
<div className="flex flex-col px-1 pb-2 pt-2"> <div className="flex flex-col px-1 pb-2 pt-2">
<div className="flex flex-col space-y-1.5"> <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"> <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-xs font-semibold text-error">
<span className="text-sm font-bold text-base-content">{monsterStats.hp.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span> HP
</span>
<span className="text-sm font-bold text-base-content">
{monsterStats.hp.toLocaleString(undefined, {
maximumFractionDigits: 0,
})}
</span>
</div> </div>
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg"> <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-xs font-semibold text-info">
<span className="text-sm font-bold text-base-content">{monsterStats.spd.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span> Speed
</span>
<span className="text-sm font-bold text-base-content">
{monsterStats.spd.toLocaleString(
undefined,
{ maximumFractionDigits: 0 },
)}
</span>
</div> </div>
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg"> <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-xs font-semibold text-base-content/70">
<span className="text-sm font-bold text-base-content">{monsterStats.stance.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span> Toughness
</span>
<span className="text-sm font-bold text-base-content">
{monsterStats.stance.toLocaleString(
undefined,
{ maximumFractionDigits: 0 },
)}
</span>
</div> </div>
</div> </div>
@@ -278,7 +368,9 @@ export default function PeakBar() {
Weakness Weakness
</span> </span>
<div className="flex items-center justify-center gap-1.5 flex-wrap"> <div className="flex items-center justify-center gap-1.5 flex-wrap">
{mapMonster?.[monsterId.toString()]?.StanceWeakList?.map((icon, iconIndex) => ( {mapMonster?.[
monsterId.toString()
]?.StanceWeakList?.map((icon, iconIndex) => (
<Image <Image
key={iconIndex} key={iconIndex}
unoptimized unoptimized
@@ -294,15 +386,15 @@ export default function PeakBar() {
</div> </div>
</div> </div>
</div> </div>
) );
})} },
)}
</div> </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) => (
+102 -69
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">
<div className="space-y-3">
<div className="flex items-center justify-between pb-2 border-b border-base-content/10">
<div className="flex items-center gap-2">
<span className="w-1 h-3.5 bg-primary rounded-full" />
<h3 className="text-xs font-bold uppercase tracking-wider text-base-content/90">
{transI18n("character")}
</h3>
</div>
<span className="text-xs font-mono font-medium text-base-content/60">
{transI18n("level")} {avatarData?.level || 1}
</span>
</div>
<div className="grid grid-cols-2 gap-x-6 gap-y-2 text-xs">
{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
<div className="flex flex-row items-center"> 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 <NextImage
src={`${process.env.CDN_URL}/${stat?.icon}`} src={`${process.env.CDN_URL}/${stat?.icon}`}
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
alt="Stat Icon" alt={stat.name}
width={40} width={20}
height={40} height={20}
className="h-10 w-10 p-1 mx-1 bg-black/20 rounded-full" className="h-5 w-5 theme-stat-icon shrink-0 object-contain"
/> />
<div className="font-bold">{stat.name}</div> <span className="font-medium text-base-content/80 truncate">{stat.name}</span>
</div> </div>
<div className="ml-3 mr-3 grow border rounded opacity-50" /> <span className="font-mono font-semibold text-primary shrink-0 text-right">
<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}
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>
}{stat.unit}</div> </span>
</div> </div>
) )
})} })}
<hr /> </div>
</div> </div>
<div className="flex flex-col items-center gap-1 w-full my-2"> {/* Set Effects - Flat Minimalist Row */}
{relicEffects.length > 0 && (
<div className="space-y-2 pt-2">
<div className="flex items-center gap-2 pb-1 border-b border-base-content/10">
<span className="w-1 h-3.5 bg-amber-500 rounded-full" />
<h4 className="text-xs font-bold uppercase tracking-wider text-base-content/90">
{transI18n("relics")}
</h4>
</div>
<div className="space-y-1">
{relicEffects.map((setEffect, index) => { {relicEffects.map((setEffect, index) => {
const relicInfo = mapRelicSet[setEffect.key]; const relicInfo = mapRelicSet[setEffect.key];
if (!relicInfo) return null; if (!relicInfo) return null;
return ( return (
<div key={index} className="flex w-full flex-row justify-between text-left">
<div <div
className="font-bold truncate max-w-full mr-1" key={index}
style={{ className="flex items-center justify-between text-xs py-1.5 border-b border-base-content/5"
fontSize: 'clamp(0.5rem, 2vw, 1rem)' >
}} <div
className="font-medium text-base-content/90 truncate max-w-[80%]"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: replaceByParam( __html: replaceByParam(getLocaleName(locale, relicInfo.Name), [])
getLocaleName(locale, relicInfo.Name),
[]
)
}} }}
/> />
<div> <span className="font-mono font-semibold text-amber-600 dark:text-amber-400 text-xs shrink-0">
<span className="black-blur bg-black/20 flex w-5 justify-center rounded px-1.5 py-0.5">{setEffect.count}</span> {setEffect.count} Pc
</div> </span>
</div> </div>
) )
})} })}
</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>
+185 -145
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;
const currentProfile = avatar.profileList[profileIndex];
if (currentProfile) {
const newProfileList = [...avatar.profileList];
newProfileList[profileIndex] = {
...currentProfile,
relics: {
...currentProfile.relics,
[selectedRelicSlot]: {
level: selectedRelicLevel, level: selectedRelicLevel,
relic_id: Number(`6${selectedRelicSet}${selectedRelicSlot}`), relic_id: Number(`6${selectedRelicSet}${selectedRelicSlot}`),
relic_set_id: Number(selectedRelicSet), relic_set_id: Number(selectedRelicSet),
main_affix_id: Number(selectedMainStat), main_affix_id: Number(selectedMainStat),
sub_affixes: listSelectedSubStats.map((item) => { sub_affixes: listSelectedSubStats.map((item) => ({
return {
sub_affix_id: Number(item.affixId), sub_affix_id: Number(item.affixId),
count: item.rollCount, count: item.rollCount,
step: item.stepCount 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 className="pb-3 border-b border-base-content/10">
<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> </div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-2">
{/* 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,103 +339,111 @@ 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">
<span className="font-bold text-amber-500">{key}-Piece:</span>
<div <div
className="text-warning leading-relaxed font-bold" className="text-base-content/80 leading-relaxed pl-1"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: replaceByParam( __html: replaceByParam(
getLocaleName(locale, value.Desc), getLocaleName(locale, value.Desc),
value.Param || [] value.Param || []
) )
}} }}
/> </span> />
</div> </div>
)) : <p className="text-blue-300 text-sm font-bold mb-1">{transI18n("pleaseSelectASet")}</p>} ))
) : (
<p className="text-base-content/40 italic py-1">{transI18n("pleaseSelectASet")}</p>
)}
</div> </div>
{/* Effect Bonus & Level */}
{/* Rarity */} <div className="space-y-3 pt-1">
<div className="grid grid-cols-2 items-center gap-4 mb-6"> <div className="flex items-center justify-between text-xs font-semibold">
<label className="block text-lg font-medium mb-2">{transI18n("rarity")}: {5} ⭐</label> <span className="text-base-content/75">{transI18n("effectBonus")}:</span>
<label className="block text-lg font-medium mb-2">{transI18n("effectBonus")}: <span className="text-warning font-bold">{effectBonus}</span></label> <span className="font-mono text-primary font-bold text-sm">+{effectBonus}</span>
</div> </div>
{/* Level */} <div className="space-y-1.5">
<div className="mb-6"> <div className="flex items-center justify-between text-xs font-semibold">
<label className="block text-lg font-medium mb-2">{transI18n("level")}</label> <span className="text-base-content/75">{transI18n("level")}</span>
<div className="bg-base-200 rounded-lg p-4"> <span className="font-mono text-primary font-bold">+{selectedRelicLevel}</span>
</div>
<input <input
type="range" type="range"
min="0" min="0"
max="15" max="15"
value={selectedRelicLevel} value={selectedRelicLevel}
onChange={(e) => setSelectedRelicLevel(parseInt(e.target.value))} onChange={(e) => setSelectedRelicLevel(parseInt(e.target.value))}
className="range range-primary w-full" className="range range-primary range-xs w-full"
/> />
<div className="text-center text-2xl font-bold mt-2">{selectedRelicLevel}</div>
</div> </div>
</div> </div>
{/* Error Message */}
<AnimatePresence> <AnimatePresence>
{error && ( {error && (
<motion.p <motion.p
key="error" key="error"
initial={{ x: 0 }} initial={{ opacity: 0, y: -4 }}
animate={{ x: [0, -2, 2, -2, 2, 0] }} animate={{ opacity: 1, y: 0 }}
transition={{ exit={{ opacity: 0 }}
duration: 6, className="text-xs text-error font-bold text-center bg-error/10 p-2 rounded-lg border border-error/20"
repeat: Infinity,
ease: "easeInOut",
repeatDelay: 1.5,
}}
className="text-error my-2 text-center font-bold"
> >
{error}! {error}
</motion.p> </motion.p>
)} )}
</AnimatePresence> </AnimatePresence>
{/* Save Button */} {/* Save Button */}
<button onClick={handlerSaveRelic} className="btn btn-success w-full"> <button
onClick={handlerSaveRelic}
className="btn btn-primary w-full rounded-xl font-bold shadow-xs mt-2"
>
{transI18n("save")} {transI18n("save")}
</button> </button>
</div> </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">
{listSelectedSubStats.map((v, index) => (
<div key={index} className="p-3.5 rounded-2xl bg-base-200/40 border border-base-content/10 space-y-2.5 shadow-xs">
<div className="flex items-center justify-between gap-2">
<div className="flex-1">
<SelectCustomImage <SelectCustomImage
customSet={Object.entries(subAffixOptions).map(([key, value]) => ({ customSet={Object.entries(subAffixOptions).map(([key, value]) => ({
value: key, value: key,
@@ -399,98 +461,76 @@ export default function RelicMaker() {
/> />
</div> </div>
{/* Current Value */} {/* Stat Value Readout */}
<div className="col-span-4 text-center flex items-center justify-center gap-2"> <div className="px-3 py-1 rounded-xl bg-base-100 border border-base-content/10 text-right min-w-24 shadow-xs">
<span className="text-2xl font-mono">+{ }</span> <span className="text-sm font-mono font-extrabold text-primary block">
<div className="text-xl font-bold text-info">{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount, v.rollCount)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}</div> +{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount, v.rollCount)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
</div> </span>
<span className="text-[11px] text-base-content/60 font-mono font-semibold">
{/* Up Roll Values */} Roll: {v.rollCount} | Step: {v.stepCount}
<div className="col-span-12"> </span>
<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 className="grid grid-cols-3 gap-1">
<button
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 0)}
className="btn btn-sm btn-info border-0"
>
{calcAffixBonus(subAffixOptions[v.affixId], 0, v.rollCount + 1)}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""}
</button>
<button
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 1)}
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">
<ChevronUp className="w-3.5 h-3.5" />
<span>{transI18n("upRoll")}</span>
</div> </div>
<div className="grid grid-cols-3 gap-1"> <div className="grid grid-cols-3 gap-1.5">
{[0, 1, 2].map((stepOffset) => (
<button <button
onClick={() => handleSubStatChange(v.affixId, index, Math.max(v.rollCount - 1, 0), Math.max(v.stepCount, 0))} key={stepOffset}
className="btn btn-sm btn-info border-0" 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], 0, Math.max(v.rollCount - 1, 0))}{mappingStats?.[subAffixOptions[v.affixId]?.Property]?.unit || ""} +{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + stepOffset, v.rollCount + 1)}
</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> </button>
))}
</div> </div>
</div> </div>
{/* Reset Button & Roll Info */} {/* Down Rolls */}
<div className="col-span-12 text-center w-full"> <div className="space-y-1">
<div className="grid grid-rows-2 gap-1 items-center justify-items-start w-full"> <div className="flex items-center gap-1 text-[11px] font-bold text-error">
<div className="grid grid-cols-2 gap-2 items-center w-full"> <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 <button
className="btn btn-error btn-sm mb-1" key={stepOffset}
onClick={() => resetSubStat(index)} 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"
> >
{transI18n("reset")} -{calcAffixBonus(subAffixOptions[v.affixId], Math.max(v.stepCount - stepOffset, 0), Math.max(v.rollCount - 1, 0))}
</button> </button>
))}
</div>
</div>
</div>
{/* Reset & Rollback actions */}
<div className="flex items-center justify-end gap-2 pt-1 border-t border-base-content/10">
<button <button
className="btn btn-warning btn-sm mb-1" className="btn btn-ghost btn-xs text-warning hover:bg-warning/10 rounded-lg text-xs font-bold"
onClick={() => handlerRollback(index)} onClick={() => handlerRollback(index)}
> >
{transI18n("rollBack")} {transI18n("rollBack")}
</button> </button>
</div> <button
className="btn btn-ghost btn-xs text-error hover:bg-error/10 rounded-lg text-xs font-bold"
<div className="grid grid-cols-2 gap-2 items-center w-full"> onClick={() => resetSubStat(index)}
<span className="font-bold">{transI18n("roll")}: <span className="text-info">{v.rollCount}</span></span> >
<span className="font-bold">{transI18n("step")}: <span className="text-info">{v.stepCount}</span></span> {transI18n("reset")}
</div> </button>
</div>
</div>
</div> </div>
</div> </div>
))} ))}
</div>
</div> </div>
</div> </div>
</div> </div>
+153 -85
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) => {
if (newSubAffixes[index]) {
newSubAffixes[index].affixId = item.sub_affix_id.toString(); newSubAffixes[index].affixId = item.sub_affix_id.toString();
newSubAffixes[index].property = subAffix["5"][item.sub_affix_id.toString()]?.Property || ""; newSubAffixes[index].property = subAffix["5"]?.[item.sub_affix_id.toString()]?.Property || "";
newSubAffixes[index].rollCount = item.count || 0; newSubAffixes[index].rollCount = item.count || 0;
newSubAffixes[index].stepCount = item.step || 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,21 +225,35 @@ 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 */}
{/* Left Section - Items Grid */} <div className="lg:col-span-2 space-y-4">
<div className="lg:col-span-2"> <div className="glass-card rounded-2xl p-4 sm:p-6 border border-base-content/10 shadow-xs space-y-6">
<div className="bg-base-100 rounded-xl p-6 shadow-lg"> <div className="flex items-center justify-between pb-3 border-b border-base-content/10">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content"> <h2 className="text-lg sm:text-xl font-bold flex items-center gap-2.5">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div> <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")} {transI18n("relics")}
</span>
</h2> </h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-6"> <button
onClick={() => {
setIsOpenQuickView(true)
handleShow("quick_view_modal")
}}
className="btn btn-sm btn-outline btn-primary rounded-xl font-bold shadow-xs"
>
{transI18n("quickView")}
</button>
</div>
{/* 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("")
@@ -195,25 +262,29 @@ export default function RelicsInfo() {
} }
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
? "bg-primary/5 rounded-2xl ring-1.5 ring-primary/40"
: "hover:bg-base-content/5"
}`}
> >
<RelicCard <RelicCard
slot={item} slot={item}
avatarId={avatarSelected?.ID.toString() || ""} avatarId={avatarSelected?.ID.toString() || ""}
/> />
</div>
<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,42 +326,36 @@ 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")}
</span>
</h3> </h3>
</div>
<div className="space-y-6"> {relicEffects.length > 0 ? (
<div className="divide-y divide-base-content/10">
{relicEffects.map((setEffect, index) => { {relicEffects.map((setEffect, index) => {
const relicInfo = mapRelicSet[setEffect.key]; const relicInfo = mapRelicSet[setEffect.key];
if (!relicInfo) return null; if (!relicInfo) return null;
return ( return (
<div key={index} className="space-y-3"> <div key={index} className="py-3.5 first:pt-0 last:pb-0 space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center justify-between">
<div <div
className="font-bold text-warning" className="font-bold text-sm text-amber-500"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: replaceByParam( __html: replaceByParam(
getLocaleName(locale, relicInfo.Name), getLocaleName(locale, relicInfo.Name),
@@ -299,22 +364,22 @@ export default function RelicsInfo() {
}} }}
/> />
{setEffect.count && ( {setEffect.count && (
<span className={`text-sm text-info`}> <span className="badge badge-primary badge-sm font-mono font-bold">
({setEffect.count}) {setEffect.count} Pc
</span> </span>
)} )}
</div> </div>
<div className="space-y-2 pl-4"> <div className="space-y-2 pt-1">
{Object.entries(relicInfo.Skills).map(([requireNum, value]) => { {Object.entries(relicInfo.Skills).map(([requireNum, value]) => {
if (Number(requireNum) > Number(setEffect.count)) return null; if (Number(requireNum) > Number(setEffect.count)) return null;
return ( return (
<div key={requireNum} className="space-y-1"> <div key={requireNum} className="text-xs space-y-0.5 pl-2 border-l-2 border-primary/40">
<div className={`font-medium text-success`}> <div className="font-bold text-emerald-500">
{requireNum}-PC: {requireNum}-PC Bonus
</div> </div>
<div <div
className="text-sm text-base-content/80 leading-relaxed pl-4" className="text-base-content/80 leading-relaxed"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: replaceByParam( __html: replaceByParam(
getLocaleName(locale, value.Desc), getLocaleName(locale, value.Desc),
@@ -330,37 +395,40 @@ export default function RelicsInfo() {
) )
})} })}
</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">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400">
{title}
</h3>
</div>
)}
{content} {content}
</div> </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={{
+56 -27
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,15 +513,28 @@ 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 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>
<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)",
@@ -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>
); );
} }
+48 -62
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">
>
{/* Subtle glow overlay */}
<div className="absolute inset-0 rounded-s-lg pointer-events-none"></div>
<div className="flex relative">
<div className="absolute inset-0 rounded-lg blur-lg -z-10"></div>
<NextImage <NextImage
src={relic?.img || ""} src={relic?.img || ""}
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
width={78} width={48}
height={78} height={48}
alt="Relic Icon" alt="Relic"
className="h-19.5 w-19.5 rounded-lg" className="h-12 w-12 object-contain"
/> />
<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>
<div className=" flex w-1/6 flex-col items-center justify-center"> {/* Main Affix - Clean Display */}
<div className="relative"> <div className="flex w-20 shrink-0 flex-col items-start justify-center">
<div className="absolute inset-0 bg-yellow-500/15 rounded-full blur-md -z-10"></div> <div className="flex items-center gap-1">
{relic?.mainAffix?.detail?.icon ? (
<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>
<span className="bg-black/20 backdrop-blur-sm rounded px-1.5 py-0.5 text-xs border border-white/10"> </div>
<span className="text-[10px] font-mono text-base-content/50 ml-0.5">
+{relic?.mainAffix?.level} +{relic?.mainAffix?.level}
</span> </span>
</div> </div>
<div style={{ opacity: 0.3, height: '78px', borderLeftWidth: '1px' }}></div> {/* Sub Affixes Grid - Clean Flat Typography */}
<div className="grid flex-1 grid-cols-2 gap-x-3 gap-y-1">
<div className="grid w-[65%] m-2 grid-cols-2 gap-1">
{relic?.subAffix?.map((subAffix, index) => { {relic?.subAffix?.map((subAffix, index) => {
if (!subAffix) return null if (!subAffix) return null
const isOptimalStat = avatarInfo?.Relics?.SubAffixPropertyList?.includes(subAffix?.property)
return ( return (
<div key={index} className="flex flex-col"> <div
<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"> 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 ? ( {subAffix?.detail?.icon ? (
<NextImage <NextImage
src={`${process.env.CDN_URL}/${subAffix?.detail?.icon}` || ""} src={`${process.env.CDN_URL}/${subAffix.detail.icon}`}
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
width={32} width={14}
height={32} height={14}
alt="Sub Affix Icon" alt="Sub Affix"
className="h-6 w-6 shrink-0" className="h-3.5 w-3.5 theme-stat-icon shrink-0 object-contain"
/> />
) : ( ) : null}
<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 font-mono truncate">
<span className="text-xs text-white/50">?</span> +{subAffix?.valueAffix + (subAffix?.detail?.unit || "")}
</div>
)}
<span className="text-xs text-gray-200 ml-0.5 truncate flex-1 min-w-0">
+{subAffix?.valueAffix + subAffix?.detail?.unit}
</span> </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>
{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> </div>
</div> </div>
</>
) )
} }
+124 -106
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,54 +129,66 @@ 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">
<div className="flex items-center justify-between pb-3 border-b border-base-300">
<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("skills")} {transI18n("skills")}
</span>
</h2> </h2>
<div className="flex flex-col items-center"> <button
<button className="btn btn-success" onClick={handlerMaxAll}>{transI18n("maxAll")}</button> className="btn btn-sm btn-primary rounded-xl font-bold shadow-xs active-press"
onClick={handlerMaxAll}
>
{transI18n("maxAll")}
</button>
</div>
{/* Visual Skill Tree Container */}
<div className="flex justify-center items-center py-2">
{traceButtons && avatarSelected && ( {traceButtons && avatarSelected && (
<div className="grid col-span-4 relative w-full aspect-square"> <div className="relative w-full max-w-md aspect-square">
<Image <Image
unoptimized unoptimized
crossOrigin="anonymous" crossOrigin="anonymous"
src={`/skilltree/${avatarSelected?.BaseType?.toUpperCase()}.webp`} src={`/skilltree/${avatarSelected?.BaseType?.toUpperCase()}.webp`}
alt="" alt="Skill Tree"
width={312} width={312}
priority={true} priority={true}
height={312} height={312}
style={{ style={{
filter: (theme === "winter" || theme === "cupcake") ? "invert(1)" : "none" filter: (theme === "winter" || theme === "cupcake") ? "invert(1)" : "none"
}} }}
className={`w-full h-full object-cover rounded-xl`} className="w-full h-full object-cover"
/> />
{traceButtons.map((btn, index) => { {traceButtons.map((btn, index) => {
if (!avatarSelected?.SkillTrees?.[btn.id]) { if (!avatarSelected?.SkillTrees?.[btn.id]) {
return null return null
} }
const isSelected = skillIDSelected === btn.id;
const isLearned = !!avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID];
const isMajor = btn.size === "big" || btn.size === "memory" || btn.size === "elation";
return ( return (
<div <div
key={`${btn.id} + ${index}`} key={`${btn.id}-${index}`}
id={btn.id} id={btn.id}
className={` className={`
absolute rounded-full border border-black absolute rounded-full border border-black/70 cursor-pointer transition-all duration-150 ease-out flex justify-center items-center select-none overflow-visible
bg-no-repeat bg-contain ${btn.size === "small" ? "w-[6.5vw] h-[6.5vw] md:w-[2.2vw] md:h-[2.2vw] bg-white" : ""}
cursor-pointer transition-all duration-200 ease-in-out ${btn.size === "medium" ? "w-[8.5vw] h-[8.5vw] md:w-[3vw] md:h-[3vw] bg-white" : ""}
shadow-[0_0_5px_white] flex justify-center items-center ${btn.size === "big" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
hover:scale-110z-10 ${btn.size === "special" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-white" : ""}
${btn.size === "small" ? "w-[6vw] h-[6vw] md:w-[2vw] md:h-[2vw] bg-white" : ""} ${btn.size === "memory" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${btn.size === "medium" ? "w-[8vw] h-[8vw] md:w-[3vw] md:h-[3vw] bg-white" : ""} ${btn.size === "elation" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${btn.size === "big" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""} ${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"}
${btn.size === "special" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-white" : ""} ${!isLearned ? "opacity-40 grayscale" : "shadow-[0_0_6px_rgba(255,255,255,0.4)]"}
${btn.size === "memory" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${btn.size === "elation" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${skillIDSelected === btn.id ? "border-4 border-primary" : ""}
${!avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID]
? "opacity-50 cursor-not-allowed"
: ""}
`} `}
onClick={() => { onClick={() => {
setSkillIDSelected(btn.id === skillIDSelected ? null : btn.id) setSkillIDSelected(btn.id === skillIDSelected ? null : btn.id)
@@ -185,97 +210,89 @@ export default function SkillsInfo() {
style={{ style={{
objectFit: "contain", objectFit: "contain",
padding: "2px", padding: "2px",
filter: (btn.size !== "big" && btn.size !== "memory" && btn.size !== "elation") ? "brightness(0%)" : "" filter: !isMajor ? "brightness(0%)" : ""
}} }}
/> />
{(btn.size === "big" || btn.size === "memory" || btn.size === "elation") && (
<p className="
z-12 text-sm sm:text-xs lg:text-sm xl:text-base 2xl:text-2xl
font-bold text-center rounded-full absolute
translate-y-full mt-1
bg-base-300 px-1
left-1/2 transform -translate-x-1/2
">
{`${avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] || 0}/${avatarSkillTree?.[btn.id]?.["1"]?.MaxLevel}`}
</p>
)} {/* Skill Type Color Overlays */}
{btn.size === "special" && ( {btn.size === "special" && (
<div <div
className="absolute inset-0 rounded-full pointer-events-none"
style={{ style={{
position: "absolute",
inset: 0,
backgroundColor: "#4a6eff", backgroundColor: "#4a6eff",
mixBlendMode: "screen", mixBlendMode: "screen",
opacity: 0.8, opacity: 0.8,
borderRadius: "50%"
}} }}
/> />
)} )}
{btn.size === "memory" && ( {btn.size === "memory" && (
<div <div
className="absolute inset-0 rounded-full pointer-events-none"
style={{ style={{
position: "absolute",
inset: 0,
backgroundColor: "#9a89ff", backgroundColor: "#9a89ff",
mixBlendMode: "screen", mixBlendMode: "screen",
opacity: 0.4, opacity: 0.45,
borderRadius: "50%"
}} }}
/> />
)} )}
{btn.size === "elation" && ( {btn.size === "elation" && (
<div <div
className="absolute inset-0 rounded-full pointer-events-none"
style={{ style={{
position: "absolute",
inset: 0,
backgroundColor: "#ff8c00", backgroundColor: "#ff8c00",
mixBlendMode: "screen", mixBlendMode: "screen",
opacity: 0.5, opacity: 0.5,
borderRadius: "50%"
}} }}
/> />
)} )}
{btn.size === "big" && ( {btn.size === "big" && (
<div <div
className="absolute inset-0 rounded-full pointer-events-none"
style={{ style={{
position: "absolute",
inset: 0,
backgroundColor: "#f5e4b0", backgroundColor: "#f5e4b0",
mixBlendMode: "screen", mixBlendMode: "screen",
opacity: 0.3, opacity: 0.35,
borderRadius: "50%"
}} }}
/> />
)} )}
{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">
{`${avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] || 0}/${avatarSkillTree?.[btn.id]?.["1"]?.MaxLevel}`}
</p>
)}
</div> </div>
) )
})} })}
</div> </div>
)} )}
{!traceButtons && avatarSelected && (
<div className="flex flex-col relative w-full aspect-square">
</div> </div>
)} </div>
</div> </div>
</div> {/* Right Panel: Skill Details */}
<div className="bg-base-100 rounded-xl p-6 shadow-lg"> <div className="lg:col-span-6 space-y-4">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content"> <div className="glass-card rounded-2xl p-4 sm:p-6 border border-base-300 shadow-sm space-y-5">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div> <div className="pb-3 border-b border-base-300">
<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-secondary to-primary rounded-full"></div>
<span className="bg-clip-text text-transparent bg-linear-to-r from-secondary to-primary font-bold tracking-tight">
{transI18n("details")} {transI18n("details")}
</span>
</h2> </h2>
{skillIDSelected && avatarSelected?.SkillTrees && avatarData && ( </div>
<div>
{skillIDSelected && avatarSelected?.SkillTrees && avatarData ? (
<div className="space-y-4">
{/* Level Slider */}
{skillInfo?.MaxLevel && skillInfo?.MaxLevel > 1 ? ( {skillInfo?.MaxLevel && skillInfo?.MaxLevel > 1 ? (
<div> <div className="space-y-1.5 pb-2">
<div className="font-bold text-success">{transI18n("level")}</div> <div className="text-sm font-bold text-base-content/80">
<div className="w-full max-w-xs"> {transI18n("level")}
<input type="range" </div>
<div className="w-full max-w-sm space-y-1">
<input
type="range"
min={1} min={1}
max={skillInfo?.MaxLevel || 1} max={skillInfo?.MaxLevel || 1}
value={avatarData?.data.skills?.[skillInfo?.PointID] || 1} value={avatarData?.data.skills?.[skillInfo?.PointID] || 1}
@@ -284,21 +301,23 @@ export default function SkillsInfo() {
newData.data.skills[skillInfo?.PointID] = parseInt(e.target.value) newData.data.skills[skillInfo?.PointID] = parseInt(e.target.value)
setAvatar(newData) setAvatar(newData)
}} }}
className="range range-success" className="range range-primary range-xs w-full"
step="1" /> step="1"
<div className="flex justify-between px-2.5 mt-2 text-xs"> />
{Array.from({ length: skillInfo?.MaxLevel }, (_, index) => index + 1).map((index) => ( <div className="flex justify-between px-1 text-[11px] font-mono text-base-content/50">
<span key={index}>{index}</span> {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-2"> <div className="flex items-center gap-3 pb-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) {
@@ -315,33 +334,35 @@ export default function SkillsInfo() {
handlerChangeStatusTrace(e.target.checked) handlerChangeStatusTrace(e.target.checked)
}} }}
/> />
<div className="font-bold text-success"> <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")} {avatarData?.data.skills?.[skillInfo?.PointID] === 1 ? transI18n("active") : transI18n("inactive")}
</span>
</div> </div>
</div> ) : null}
) : (
null
)}
{/* Point Name & Stat Additions */}
{((skillInfo?.PointName && skillInfo?.PointDesc) || {((skillInfo?.PointName && skillInfo?.PointDesc) ||
(skillInfo?.PointName && skillInfo?.StatusAddList.length > 0)) (skillInfo?.PointName && skillInfo?.StatusAddList.length > 0)) && (
&& ( <div className="space-y-1.5">
<div className="text-xl font-bold flex items-center gap-2 mt-2"> <h3 className="text-lg font-extrabold text-base-content">
{getLocaleName(locale, skillInfo.PointName)} {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>
)} )}
{/* Skill Description Prose */}
{skillInfo?.PointDesc && ( {skillInfo?.PointDesc && (
<div <div
className="text-xs sm:text-sm text-base-content/85 leading-relaxed"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: replaceByParam( __html: replaceByParam(
getLocaleName(locale, skillInfo?.PointDesc) || "", getLocaleName(locale, skillInfo?.PointDesc) || "",
@@ -351,35 +372,32 @@ export default function SkillsInfo() {
/> />
)} )}
{skillInfo?.LevelUpSkillID {/* Level-up Skill Items (Major / Servant Skills) */}
&& skillInfo?.LevelUpSkillID.length > 0 {skillInfo?.LevelUpSkillID && skillInfo?.LevelUpSkillID.length > 0 && dataLevelUpSkill && (
&& dataLevelUpSkill <div className="space-y-6 pt-1">
&& (
<div className="mt-2 flex flex-col gap-2">
{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>
); );
} }
+30 -13
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 || {};
const skillName = getLocaleName(locale, skill.Name);
return ( return (
<div className="flex flex-col gap-2"> <div className="space-y-2.5">
<div className="space-y-2 pb-2"> {/* Skill Name Heading */}
<div className="flex items-center gap-2"> <h3 className="text-xl font-bold text-base-content tracking-tight">
<div className="w-1 h-5 bg-primary/80 rounded-sm" /> {skillName}
<div </h3>
className="text-lg font-bold tracking-wide text-foreground uppercase"
dangerouslySetInnerHTML={{ __html: replaceByParam(getLocaleName(locale, skill.Name), []) }} {/* 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 <div
className="text-[15px] leading-relaxed text-foreground/90 pl-3 border-l border-transparent" className="text-xs sm:text-sm leading-relaxed text-base-content/85"
dangerouslySetInnerHTML={{ __html: replaceByParam(descHtml, params) }} dangerouslySetInnerHTML={{ __html: replaceByParam(descHtml, params) }}
/> />
</div>
{/* 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'],
+23 -8
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,11 +7,27 @@ 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'],
@@ -20,14 +35,14 @@ export const useFetchAvatarData = () => {
staleTime: 1000 * 60 * 5, staleTime: 1000 * 60 * 5,
}) })
useEffect(() => { useEffect(() => {
if (!mapAvatar || Object.keys(mapAvatar).length === 0) return
const listAvatarId = Object.keys(avatars) const listAvatarId = Object.keys(avatars)
const listAvatarNotExist = Object.entries(mapAvatar).filter(([avatarId]) => !listAvatarId.includes(avatarId)) const listAvatarNotExist = Object.entries(mapAvatar).filter(([avatarId]) => !listAvatarId.includes(avatarId))
if (listAvatarNotExist.length === 0) return
const avatarStore = converterToAvatarStore(Object.fromEntries(listAvatarNotExist)) const avatarStore = converterToAvatarStore(Object.fromEntries(listAvatarNotExist))
if (Object.keys(avatarStore).length === 0) return if (Object.keys(avatarStore).length === 0) return
for (const avatar of Object.values(avatarStore)) { setAvatars({ ...avatars, ...avatarStore })
setAvatar(avatar) }, [mapAvatar, avatars, setAvatars])
}
}, [mapAvatar])
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 }),
+14 -21
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,9 +166,9 @@ const useRelicMakerStore = create<RelicMakerState>((set) => ({
], ],
} }
} }
} else if (index !== null) { }
const newPreSelectedSubStats = { ...state.preSelectedSubStats }; const newPreSelectedSubStats = { ...state.preSelectedSubStats };
newPreSelectedSubStats[index] = [ newPreSelectedSubStats[index.toString()] = [
{ {
property: "", property: "",
affixId: "", affixId: "",
@@ -179,34 +179,27 @@ const useRelicMakerStore = create<RelicMakerState>((set) => ({
return { return {
preSelectedSubStats: newPreSelectedSubStats preSelectedSubStats: newPreSelectedSubStats
} }
}
return {}
}), }),
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);
newPreSelectedSubStats[index] = copied;
return { preSelectedSubStats: newPreSelectedSubStats }; 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;
}