Refactor TypeScript interfaces and types across multiple files for consistency and clarity
Gitea Auto Deploy / Deploy-Container (push) Failing after 46s

- Updated metadata interfaces in `metaData.ts` for improved readability.
- Refactored `mics.ts` types to enhance structure and maintainability.
- Added `Invasion` type to `mocDetail.ts` and `peakDetail.ts` for new functionality.
- Simplified modal configuration in `modelConfig.ts`.
- Cleaned up monster detail interfaces in `monsterDetail.ts`.
- Enhanced battle configuration types in `pfDetail.ts` to include `Invasion`.
- Updated JSON schemas in `srtools.zod.ts` to use `nullish` for optional fields.
- Introduced new skills-lock.json for skill management.
- Added global type declarations for CSS and SCSS modules in `global.d.ts`.
This commit is contained in:
2026-08-27 00:41:35 +07:00
parent 7699ac7139
commit 73b6fc268f
133 changed files with 16116 additions and 14971 deletions
+24 -24
View File
@@ -1,25 +1,25 @@
import { getDataCache } from "@/lib/cache/cache"
export async function GET(
req: Request,
{ params }: { params: Promise<{ name: string }> }
) {
const { name } = await params
const item = getDataCache(name)
if (!item) {
return new Response("Not found", { status: 404 })
}
const headers: Record<string, string> = {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=3600"
}
if (item.type === "br") {
headers["Content-Encoding"] = "br"
}
return new Response(new Uint8Array(item.buf), { headers })
import { getDataCache } from "@/lib/cache/cache"
export async function GET(
req: Request,
{ params }: { params: Promise<{ name: string }> }
) {
const { name } = await params
const item = getDataCache(name)
if (!item) {
return new Response("Not found", { status: 404 })
}
const headers: Record<string, string> = {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=3600"
}
if (item.type === "br") {
headers["Content-Encoding"] = "br"
}
return new Response(new Uint8Array(item.buf), { headers })
}
+70 -70
View File
@@ -1,79 +1,79 @@
import { NextRequest, NextResponse } from 'next/server'
import axios from 'axios'
import net from 'net'
function isPrivateHost(hostname: string): boolean {
if (
hostname === 'localhost' ||
hostname.startsWith('127.') ||
hostname.startsWith('10.') ||
hostname.startsWith('192.168.') ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname)
) {
return true
}
if (net.isIP(hostname)) {
return true
}
return false
}
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const targetUrl = searchParams.get("url")
if (!targetUrl) {
return NextResponse.json({ error: "Missing url" }, { status: 400 })
}
const response = await fetch(targetUrl)
if (!response.ok) {
return NextResponse.json({ error: "Failed to fetch image" }, { status: 500 })
}
const buffer = await response.arrayBuffer()
return new NextResponse(buffer, {
headers: {
"Content-Type": response.headers.get("content-type") || "image/png",
"Cache-Control": "public, max-age=3600",
"Access-Control-Allow-Origin": "*",
},
})
} catch (e: unknown) {
return NextResponse.json({ error: (e as Error)?.message }, { status: 500 })
}
}
function isPrivateHost(hostname: string): boolean {
if (
hostname === 'localhost' ||
hostname.startsWith('127.') ||
hostname.startsWith('10.') ||
hostname.startsWith('192.168.') ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname)
) {
return true
}
if (net.isIP(hostname)) {
return true
}
return false
}
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url)
const targetUrl = searchParams.get("url")
if (!targetUrl) {
return NextResponse.json({ error: "Missing url" }, { status: 400 })
}
const response = await fetch(targetUrl)
if (!response.ok) {
return NextResponse.json({ error: "Failed to fetch image" }, { status: 500 })
}
const buffer = await response.arrayBuffer()
return new NextResponse(buffer, {
headers: {
"Content-Type": response.headers.get("content-type") || "image/png",
"Cache-Control": "public, max-age=3600",
"Access-Control-Allow-Origin": "*",
},
})
} catch (e: unknown) {
return NextResponse.json({ error: (e as Error)?.message }, { status: 500 })
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { serverUrl, method, ...payload } = body
if (!serverUrl) {
return NextResponse.json({ error: 'Missing serverUrl' }, { status: 400 })
}
if (!method) {
return NextResponse.json({ error: 'Missing method' }, { status: 400 })
}
let url = serverUrl.trim()
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = `http://${url}`
}
const parsed = new URL(url)
if (isPrivateHost(parsed.hostname)) {
return NextResponse.json(
{ error: `Connection to private/internal address (${parsed.hostname}) is not allowed` },
{ status: 403 }
)
}
const { serverUrl, method, ...payload } = body
if (!serverUrl) {
return NextResponse.json({ error: 'Missing serverUrl' }, { status: 400 })
}
if (!method) {
return NextResponse.json({ error: 'Missing method' }, { status: 400 })
}
let url = serverUrl.trim()
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = `http://${url}`
}
const parsed = new URL(url)
if (isPrivateHost(parsed.hostname)) {
return NextResponse.json(
{ error: `Connection to private/internal address (${parsed.hostname}) is not allowed` },
{ status: 403 }
)
}
let response
switch (method.toUpperCase()) {
+9 -9
View File
@@ -1,9 +1,9 @@
"use client"
import EidolonsInfo from "@/components/eidolonsInfo";
export default function EidolonsInfoPage() {
return (
<div className="w-full">
<EidolonsInfo/>
</div>
);
}
"use client"
import EidolonsInfo from "@/components/eidolonsInfo";
export default function EidolonsInfoPage() {
return (
<div className="w-full">
<EidolonsInfo/>
</div>
);
}
+80 -8
View File
@@ -1,6 +1,5 @@
@import "tailwindcss";
@plugin "daisyui" {
exclude: properties;
themes: winter --default, night --prefersdark, cupcake, coffee;
@@ -9,19 +8,19 @@
@tailwind utilities;
@plugin 'tailwind-scrollbar' {
nocompatible: true;
nocompatible: true;
}
:root {
--size-big: 4vw;
--size-medium: 3vw;
--size-small: 2vw;
}
/* Smooth Modern Ultra-Sleek Scrollbars */
::-webkit-scrollbar {
width: 6px;
height: 6px;
width: 4px;
height: 4px;
}
::-webkit-scrollbar-track {
@@ -29,10 +28,83 @@
}
::-webkit-scrollbar-thumb {
background-color: #9ca3af;
border-radius: 0;
background: rgba(156, 163, 175, 0.28);
border-radius: 9999px;
transition: background-color 0.2s ease;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(156, 163, 175, 0.55);
}
::-webkit-scrollbar-button {
display: none;
display: none;
}
/* Custom UI Utilities for Gaming Companion Aesthetic */
@layer utilities {
.glass-card {
background-color: var(--color-base-100);
border: 1px solid color-mix(in srgb, var(--color-base-content) 10%, transparent);
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
}
.glass-panel {
background-color: var(--color-base-100);
border: 1px solid color-mix(in srgb, var(--color-base-content) 10%, transparent);
}
.hover-lift {
transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.25s cubic-bezier(0.16, 1, 0.3, 1), border-color 0.25s ease;
}
.hover-lift:hover {
transform: translateY(-2px);
}
.active-press {
transition: transform 0.15s cubic-bezier(0.16, 1, 0.3, 1);
}
.active-press:active {
transform: scale(0.97);
}
/* Rarity Glow Effects */
.glow-5star {
box-shadow: 0 0 16px -2px rgba(245, 158, 11, 0.35);
}
.glow-4star {
box-shadow: 0 0 16px -2px rgba(168, 85, 247, 0.35);
}
/* Ambient Background - Clean Canvas */
.bg-ambient-mesh {
background-color: var(--color-base-100);
}
/* Theme Adaptive Path & Stat Icon Filters - High Accessibility Contrast */
.theme-path-icon {
filter: brightness(0.2) opacity(0.85);
}
[data-theme="night"] .theme-path-icon,
[data-theme="coffee"] .theme-path-icon {
filter: brightness(0.8) opacity(0.85);
}
.theme-path-icon-active {
filter: brightness(0);
}
[data-theme="night"] .theme-path-icon-active,
[data-theme="coffee"] .theme-path-icon-active {
filter: brightness(1);
}
/* Stat icons (HSR raw sprite icons are white pngs, need dark filter on light themes) */
.theme-stat-icon {
filter: brightness(0.15) opacity(0.8);
}
[data-theme="night"] .theme-stat-icon,
[data-theme="coffee"] .theme-stat-icon {
filter: brightness(0.95) opacity(1);
}
}
+12 -10
View File
@@ -64,9 +64,9 @@ export default async function RootLayout({
const locale = await getLocale()
return (
<html lang={locale} suppressHydrationWarning>
<html lang={locale} suppressHydrationWarning className="overflow-x-hidden">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${geistSans.variable} ${geistMono.variable} antialiased overflow-x-hidden max-w-full`}
suppressHydrationWarning
>
<NextIntlClientProvider messages={messages}>
@@ -74,17 +74,19 @@ export default async function RootLayout({
<ThemeProvider>
<ClientThemeWrapper>
<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 />
<div className="grid grid-cols-12 w-full">
<div className="hidden sm:block md:col-span-4 lg:col-span-3 sticky top-0 self-start h-fit">
<main className="grid grid-cols-12 w-full flex-1">
<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 />
</div>
<div className="col-span-12 sm:col-span-8 lg:col-span-9">
</aside>
<section className="col-span-12 sm:col-span-8 md:col-span-8 lg:col-span-9 flex flex-col min-w-0">
<ActionBar />
{children}
</div>
</div>
<div className="flex-1 p-2 sm:p-3 sm:pt-1">
{children}
</div>
</section>
</main>
<Footer />
</div>
+11 -11
View File
@@ -1,11 +1,11 @@
"use client";
import RelicsInfo from "@/components/relicsInfo";
export default function RelicsInfoPage() {
return (
<div className="w-full">
<RelicsInfo></RelicsInfo>
</div>
);
}
"use client";
import RelicsInfo from "@/components/relicsInfo";
export default function RelicsInfoPage() {
return (
<div className="w-full">
<RelicsInfo></RelicsInfo>
</div>
);
}
+11 -11
View File
@@ -1,11 +1,11 @@
"use client"
import ShowCaseInfo from "@/components/showcaseCard"
export default function ShowcaseCard() {
return (
<div>
<ShowCaseInfo />
</div>
)
}
"use client"
import ShowCaseInfo from "@/components/showcaseCard"
export default function ShowcaseCard() {
return (
<div>
<ShowCaseInfo />
</div>
)
}
+11 -11
View File
@@ -1,11 +1,11 @@
"use client";
import SkillsInfo from "@/components/skillsInfo";
export default function SkillsInfoPage() {
return (
<div className="w-full">
<SkillsInfo></SkillsInfo>
</div>
);
}
"use client";
import SkillsInfo from "@/components/skillsInfo";
export default function SkillsInfoPage() {
return (
<div className="w-full">
<SkillsInfo></SkillsInfo>
</div>
);
}
+509 -444
View File
@@ -1,445 +1,510 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import { useTranslations } from "next-intl";
import Image from "next/image";
import { useRouter } from 'next/navigation'
import ParseText from "../parseText";
import useLocaleStore from "@/stores/localeStore";
import { getNameChar } from "@/helper/getName";
import { useEffect, useMemo, useState } from "react";
import { motion } from "framer-motion";
import useModelStore from "@/stores/modelStore";
import useUserDataStore from "@/stores/userDataStore";
import { ModalConfig, RelicStore } from "@/types";
import { toast } from "react-toastify";
import useGlobalStore from "@/stores/globalStore";
import { connectToPS, syncDataToPS } from "@/helper";
import CopyImport from "../importBar/copy";
import useCopyProfileStore from "@/stores/copyProfile";
import AvatarBar from "../avatarBar";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
export default function ActionBar() {
const router = useRouter()
const { avatarSelected } = useCurrentDataStore()
const { damageType, baseType } = useDetailDataStore()
const { setResetData } = useCopyProfileStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore()
const {
isOpenCreateProfile,
setIsOpenCreateProfile,
isOpenCopy,
setIsOpenCopy,
isOpenAvatars,
setIsOpenAvatars
} = useModelStore()
const { avatars, setAvatar } = useUserDataStore()
const [profileName, setProfileName] = useState("");
const [formState, setFormState] = useState("EDIT");
const [profileEdit, setProfileEdit] = useState(-1);
const { isConnectPS } = useGlobalStore()
const profileCurrent = useMemo(() => {
if (!avatarSelected) return null;
const avatar = avatars[avatarSelected.ID];
return avatar?.profileList[avatar.profileSelect] || null;
}, [avatarSelected, avatars]);
const listProfile = useMemo(() => {
if (!avatarSelected) return [];
const avatar = avatars[avatarSelected.ID];
return avatar?.profileList || [];
}, [avatarSelected, avatars]);
const handleUpdateProfile = () => {
if (!profileName.trim()) return;
if (formState === "CREATE" && avatarSelected && avatars[avatarSelected.ID]) {
const newListProfile = [...listProfile]
const newProfile = {
profile_name: profileName,
lightcone: null,
relics: {} as Record<string, RelicStore>
}
newListProfile.push(newProfile)
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: newListProfile.length - 1 })
toast.success("Profile created successfully")
} else if (formState === "EDIT" && profileCurrent && avatarSelected && profileEdit !== -1) {
const newListProfile = [...listProfile]
newListProfile[profileEdit].profile_name = profileName;
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile })
toast.success("Profile updated successfully")
}
handleCloseModal("update_profile_modal");
};
const handleShow = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.showModal();
}
};
// Close modal handler
const handleCloseModal = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.close();
}
};
const actionMove = (path: string) => {
router.push(`/${path}`)
}
const handleProfileSelect = (profileId: number) => {
if (!avatarSelected) return;
if (avatars[avatarSelected.ID].profileSelect === profileId) return;
setAvatar({ ...avatars[avatarSelected.ID], profileSelect: profileId })
toast.success(`Profile changed to Profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}`)
}
const handleDeleteProfile = (profileId: number, e: React.MouseEvent) => {
e.stopPropagation()
if (!avatarSelected || profileId == 0) return;
if (window.confirm(`Are you sure you want to delete profile: ${avatars[avatarSelected.ID].profileList[profileId].profile_name}?`)) {
const newListProfile = [...listProfile]
newListProfile.splice(profileId, 1)
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: profileId - 1 })
toast.success(`Profile ${avatars[avatarSelected.ID].profileList[profileId].profile_name} deleted successfully`)
}
}
const handleConnectOrSyncPS = async () => {
if (isConnectPS) {
const res = await syncDataToPS()
if (res.success) {
toast.success(transI18n("syncSuccess"))
} else {
toast.error(`${transI18n("syncFailed")}: ${res.message}`)
}
} else {
const res = await connectToPS()
if (res.success) {
toast.success(transI18n("connectedSuccess"))
} else {
toast.error(`${transI18n("connectedFailed")}: ${res.message}`)
}
}
}
const modalConfigs: ModalConfig[] = [
{
id: "update_profile_modal",
title: formState === "CREATE" ? transI18n("createNewProfile") : transI18n("editProfile"),
isOpen: isOpenCreateProfile,
onClose: () => {
setIsOpenCreateProfile(false)
handleCloseModal("update_profile_modal")
},
content: (
<div className="px-6 space-y-4">
<div className="form-control">
<label className="label">
<span className="label-text text-primary font-semibold text-lg">
{transI18n("profileName")}
</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderProfileName")}
className="input input-warning mt-1 w-full"
value={profileName}
onChange={(e) => setProfileName(e.target.value)}
/>
</div>
<div className="modal-action">
<button className="btn btn-success btn-sm sm:btn-md" onClick={handleUpdateProfile}>
{formState === "CREATE" ? transI18n("create") : transI18n("update")}
</button>
</div>
</div>
)
},
{
id: "copy_profile_modal",
title: transI18n("copyProfiles").toUpperCase(),
isOpen: isOpenCopy,
onClose: () => {
setIsOpenCopy(false)
handleCloseModal("copy_profile_modal")
},
content: <CopyImport />
},
{
id: "avatars_modal",
title: transI18n("avatars").toUpperCase(),
isOpen: isOpenAvatars,
onClose: () => {
setIsOpenAvatars(false)
handleCloseModal("avatars_modal")
},
content: <AvatarBar onClose={() => { setIsOpenAvatars(false); handleCloseModal("avatars_modal") }} />
}
]
// Handle ESC key to close modal
useEffect(() => {
for (const item of modalConfigs) {
if (!item?.isOpen) {
handleCloseModal(item?.id || "")
}
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
for (const item of modalConfigs) {
handleCloseModal(item?.id || "")
}
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenCopy, isOpenCreateProfile, isOpenAvatars]);
return (
<div className="w-full px-4 pb-4 bg-base-200">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 items-center justify-items-center">
<div className="flex flex-col justify-center w-full">
<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 && (
<div className="flex flex-wrap items-center justify-start h-full w-full">
<Image
src={`${process.env.CDN_URL}/${damageType?.[avatarSelected?.DamageType]?.Icon}`}
alt={'damage type'}
unoptimized
crossOrigin="anonymous"
className="h-10 w-10 object-contain"
width={100}
height={100}
/>
<p className="text-center font-bold text-lg">
{transI18n(avatarSelected.BaseType.toLowerCase())}
</p>
<div className="text-center font-bold text-lg">{" / "}</div>
<ParseText
locale={locale}
text={getNameChar(locale, transI18n, avatarSelected).toWellFormed()}
className={"font-bold text-lg"}
/>
<div className="text-center italic text-sm ml-2"> {`(${avatarSelected.ID})`}</div>
</div>
)}
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-base opacity-70 font-bold w-16">{transI18n("profile")}:</span>
<div className="dropdown dropdown-center md:dropdown-start">
<div
tabIndex={0}
role="button"
className="btn btn-warning border-info btn-soft gap-1"
>
<span className="truncate max-w-48 font-bold">
{profileCurrent?.profile_name}
</span>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</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">
{listProfile.map((profile, index) => (
<li key={index} className="grid grid-cols-12">
<button
className={`col-span-8 btn btn-ghost`}
onClick={() => handleProfileSelect(index)}
>
<span className="flex-1 truncate text-left">
{profile.profile_name}
{index === 0 && (
<span className="text-xs text-base-content/60 ml-1">({transI18n("default")})</span>
)}
</span>
</button>
{index !== 0 && (
<>
<button
className="col-span-2 flex items-center justify-center text-lg text-warning hover:bg-warning/20 z-20 w-full h-full"
onClick={() => {
setFormState("EDIT");
setProfileName(profile.profile_name)
setProfileEdit(index)
setIsOpenCreateProfile(true)
handleShow("update_profile_modal")
}}
title="Edit Profile"
>
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536M4 20h4l10.293-10.293a1 1 0 000-1.414l-2.586-2.586a1 1 0 00-1.414 0L4 16v4z" />
</svg>
</button>
<button
className="col-span-2 flex items-center justify-center text-error hover:bg-error/20 z-20 w-full h-full text-lg"
onClick={(e) => {
handleDeleteProfile(index, e)
}}
title="Delete Profile"
>
<svg className="w-4 h-4 " fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6M9 7V4h6v3m5 0H4" />
</svg>
</button>
</>
)}
</li>
))}
<li className="border-t border-base-300 mt-2 pt-2 z-10">
<button
onClick={() => {
setIsOpenCopy(true)
setResetData(baseType, damageType)
handleShow("copy_profile_modal")
}}
className="btn btn-ghost flex justify-start px-3 py-2 h-full w-full hover:bg-base-200 cursor-pointer text-primary z-20"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
{transI18n("copyProfiles")}
</button>
<button
className="btn btn-ghost flex justify-start px-3 py-2 h-full w-full hover:bg-base-200 cursor-pointer text-primary z-20"
onClick={() => {
setIsOpenCreateProfile(true)
setFormState("CREATE");
setProfileName("")
handleShow("update_profile_modal")
}}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
{transI18n("addNewProfile")}
</button>
</li>
</ul>
</div>
<div className=" grid grid-cols-2 w-full sm:hidden gap-2">
<button
onClick={() => {
setIsOpenAvatars(true)
handleShow("avatars_modal")
}}
className="col-span-1 btn btn-warning btn-sm w-full">
{transI18n("avatars")}
</button>
<div className="col-span-1 dropdown dropdown-center w-full">
<label tabIndex={0} className="btn btn-info btn-sm w-full">
{transI18n("actions")}
</label>
<ul
tabIndex={0}
className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box min-w-max w-full mt-1 border border-base-300 max-h-60 overflow-y-auto"
>
<li>
<button onClick={() => actionMove('')}>
{transI18n("characterInformation")}
</button>
</li>
<li>
<button onClick={() => actionMove('relics-info')}>
{transI18n("relics")}
</button>
</li>
<li>
<button onClick={() => actionMove('eidolons-info')}>
{transI18n("eidolons")}
</button>
</li>
<li>
<button onClick={() => actionMove('skills-info')}>
{transI18n("skills")}
</button>
</li>
<li>
<button onClick={() => actionMove('showcase-card')}>
{transI18n("showcaseCard")}
</button>
</li>
<li>
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
<div className="hidden sm:grid grid-cols-3 gap-2 w-full">
<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 }) => (
<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="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={onClose}
>
✕
</motion.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>
{content}
</div>
</dialog>
))}
</div>
</div>
);
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import { useTranslations } from "next-intl";
import Image from "next/image";
import { usePathname, useRouter } from 'next/navigation'
import ParseText from "../parseText";
import useLocaleStore from "@/stores/localeStore";
import { getNameChar } from "@/helper/getName";
import { useEffect, useMemo, useState } from "react";
import useModelStore from "@/stores/modelStore";
import useUserDataStore from "@/stores/userDataStore";
import { ModalConfig, RelicStore, AvatarProfileStore } from "@/types";
import { toast } from "react-toastify";
import useGlobalStore from "@/stores/globalStore";
import { connectToPS, syncDataToPS } from "@/helper";
import CopyImport from "../importBar/copy";
import useCopyProfileStore from "@/stores/copyProfile";
import AvatarBar from "../avatarBar";
import useCurrentDataStore from "@/stores/currentDataStore";
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() {
const router = useRouter()
const pathname = usePathname()
const { avatarSelected } = useCurrentDataStore(
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 { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const {
isOpenCreateProfile,
setIsOpenCreateProfile,
isOpenCopy,
setIsOpenCopy,
isOpenAvatars,
setIsOpenAvatars
} = useModelStore(
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 [formState, setFormState] = useState("EDIT");
const [profileEdit, setProfileEdit] = useState(-1);
const { isConnectPS } = useGlobalStore(
useShallow((state) => ({ isConnectPS: state.isConnectPS }))
)
const profileCurrent = useMemo(() => {
if (!avatarSelected) return null;
const avatar = avatars[avatarSelected.ID];
return avatar?.profileList?.[avatar?.profileSelect] ?? avatar?.profileList?.[0] ?? null;
}, [avatarSelected, avatars]);
const listProfile = useMemo(() => {
if (!avatarSelected) return [];
const avatar = avatars[avatarSelected.ID];
return avatar?.profileList || [];
}, [avatarSelected, avatars]);
const handleUpdateProfile = () => {
if (!profileName.trim()) return;
if (formState === "CREATE" && avatarSelected && avatars[avatarSelected.ID]) {
const newProfile: AvatarProfileStore = {
profile_name: profileName,
lightcone: null,
relics: {} as Record<string, RelicStore>
};
const newListProfile = [...listProfile, newProfile];
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile, profileSelect: newListProfile.length - 1 })
toast.success("Profile created successfully")
} else if (formState === "EDIT" && profileCurrent && avatarSelected && profileEdit !== -1) {
const newListProfile = listProfile.map((p, idx) =>
idx === profileEdit ? { ...p, profile_name: profileName } : p
);
setAvatar({ ...avatars[avatarSelected.ID], profileList: newListProfile })
toast.success("Profile updated successfully")
}
handleCloseModal("update_profile_modal");
};
const handleShow = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.showModal();
}
};
// Close modal handler
const handleCloseModal = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.close();
}
};
const actionMove = (path: string) => {
router.push(`/${path}`)
}
const handleProfileSelect = (profileId: number) => {
if (!avatarSelected || !avatars[avatarSelected.ID]) return;
if (avatars[avatarSelected.ID].profileSelect === profileId) return;
setAvatar({ ...avatars[avatarSelected.ID], profileSelect: profileId })
const selectedProfile = avatars[avatarSelected.ID].profileList?.[profileId];
if (selectedProfile) {
toast.success(`Profile changed to: ${selectedProfile.profile_name}`)
}
}
const handleDeleteProfile = (profileId: number, e: React.MouseEvent) => {
e.stopPropagation()
if (!avatarSelected || profileId === 0 || !avatars[avatarSelected.ID]) return;
const targetProfile = avatars[avatarSelected.ID].profileList?.[profileId];
if (!targetProfile) return;
if (window.confirm(`Are you sure you want to delete profile: ${targetProfile.profile_name}?`)) {
const currentSelect = avatars[avatarSelected.ID].profileSelect;
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 () => {
if (isConnectPS) {
const res = await syncDataToPS()
if (res.success) {
toast.success(transI18n("syncSuccess"))
} else {
toast.error(`${transI18n("syncFailed")}: ${res.message}`)
}
} else {
const res = await connectToPS()
if (res.success) {
toast.success(transI18n("connectedSuccess"))
} else {
toast.error(`${transI18n("connectedFailed")}: ${res.message}`)
}
}
}
const modalConfigs: ModalConfig[] = [
{
id: "update_profile_modal",
title: formState === "CREATE" ? transI18n("createNewProfile") : transI18n("editProfile"),
isOpen: isOpenCreateProfile,
onClose: () => {
setIsOpenCreateProfile(false)
handleCloseModal("update_profile_modal")
},
content: (
<div className="space-y-4">
<div className="form-control">
<label className="label">
<span className="label-text font-semibold text-base-content/80">
{transI18n("profileName")}
</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderProfileName")}
className="input input-bordered input-primary w-full"
value={profileName}
onChange={(e) => setProfileName(e.target.value)}
autoFocus
/>
</div>
<div className="flex justify-end gap-2 pt-2">
<button className="btn btn-ghost" onClick={() => setIsOpenCreateProfile(false)}>
Cancel
</button>
<button className="btn btn-primary" onClick={handleUpdateProfile}>
{formState === "CREATE" ? transI18n("create") : transI18n("update")}
</button>
</div>
</div>
)
},
{
id: "copy_profile_modal",
title: transI18n("copyProfiles"),
isOpen: isOpenCopy,
onClose: () => {
setIsOpenCopy(false)
handleCloseModal("copy_profile_modal")
},
content: <CopyImport />
},
{
id: "avatars_modal",
title: transI18n("avatars"),
isOpen: isOpenAvatars,
onClose: () => {
setIsOpenAvatars(false)
handleCloseModal("avatars_modal")
},
content: <AvatarBar onClose={() => { setIsOpenAvatars(false); handleCloseModal("avatars_modal") }} />
}
]
// Handle ESC key to close modal
useEffect(() => {
for (const item of modalConfigs) {
if (!item?.isOpen) {
handleCloseModal(item?.id || "")
}
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
for (const item of modalConfigs) {
handleCloseModal(item?.id || "")
}
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [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 (
<div className="w-full bg-transparent px-3 sm:px-4 py-2 space-y-2">
{/* Top Row: Character Identity + Profile Selector + Connect PS */}
<div className="flex flex-wrap items-center justify-between gap-2.5">
{/* Character Identity */}
{avatarSelected && (
<div className="flex items-center gap-2 h-8.5 px-3 rounded-xl bg-base-200/80 border border-base-content/10 text-xs sm:text-sm font-semibold shadow-xs">
<Image
src={`${process.env.CDN_URL}/${damageType?.[avatarSelected?.DamageType]?.Icon}`}
alt="damage type"
unoptimized
crossOrigin="anonymous"
className="h-5 w-5 object-contain"
width={20}
height={20}
/>
<span className="text-base-content/85 font-medium">{transI18n(avatarSelected.BaseType.toLowerCase())}</span>
<span className="text-base-content/30">/</span>
<ParseText
locale={locale}
text={getNameChar(locale, transI18n, avatarSelected).toWellFormed()}
className="font-bold text-primary"
/>
<span className="text-[11px] font-mono text-base-content/60 font-semibold">#{avatarSelected.ID}</span>
</div>
)}
{/* 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="dropdown dropdown-bottom dropdown-start h-full flex items-center">
<div
tabIndex={0}
role="button"
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="text-[11px] uppercase tracking-wider text-base-content/60 font-semibold">{transI18n("profile")}:</span>
<span className="truncate max-w-28 sm:max-w-40 text-xs text-base-content font-bold">
{profileCurrent?.profile_name || "Default Profile"}
</span>
<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" />
</svg>
</div>
<ul className="dropdown-content z-50 menu p-2 shadow-2xl bg-base-100 rounded-2xl min-w-60 mt-2 border border-base-300 max-h-72 overflow-y-auto space-y-1">
{listProfile.map((profile, index) => (
<li key={index} className="flex flex-row items-center justify-between rounded-xl px-1 hover:bg-base-200">
<button
className="flex-1 text-left py-2 px-2 text-xs font-medium truncate"
onClick={() => handleProfileSelect(index)}
>
<span className="truncate">{profile.profile_name}</span>
{index === 0 && (
<span className="text-[10px] text-base-content/50 ml-1.5 font-mono">({transI18n("default")})</span>
)}
</button>
{index !== 0 && (
<div className="flex items-center gap-1">
<button
className="btn btn-ghost btn-xs btn-circle text-warning hover:bg-warning/20"
onClick={() => {
setFormState("EDIT");
setProfileName(profile.profile_name)
setProfileEdit(index)
setIsOpenCreateProfile(true)
handleShow("update_profile_modal")
}}
title="Edit Profile"
>
<Edit2 className="w-3 h-3" />
</button>
<button
className="btn btn-ghost btn-xs btn-circle text-error hover:bg-error/20"
onClick={(e) => handleDeleteProfile(index, e)}
title="Delete Profile"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
)}
</li>
))}
<li className="border-t border-base-300 pt-1 mt-1">
<button
onClick={() => {
setIsOpenCopy(true)
setResetData(baseType, damageType)
handleShow("copy_profile_modal")
}}
className="flex items-center gap-2 py-2 px-3 text-xs font-semibold text-primary hover:bg-primary/10 rounded-xl"
>
<Copy className="w-3.5 h-3.5" />
{transI18n("copyProfiles")}
</button>
</li>
<li>
<button
onClick={() => {
setIsOpenCreateProfile(true)
setFormState("CREATE");
setProfileName("")
handleShow("update_profile_modal")
}}
className="flex items-center gap-2 py-2 px-3 text-xs font-semibold text-primary hover:bg-primary/10 rounded-xl"
>
<Plus className="w-3.5 h-3.5" />
{transI18n("addNewProfile")}
</button>
</li>
</ul>
</div>
{/* 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
onClick={() => {
setIsOpenCopy(true)
setResetData(baseType, damageType)
handleShow("copy_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("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")}
</button>
<div className="dropdown dropdown-end w-full">
<label tabIndex={0} className="btn btn-info btn-sm w-full font-bold shadow-xs">
{transI18n("actions")}
</label>
<ul
tabIndex={0}
className="dropdown-content z-50 menu p-2 shadow-2xl bg-base-100 rounded-2xl w-56 mt-2 border border-base-300 space-y-1"
>
{navTabs.map((tab) => {
const Icon = tab.icon;
return (
<li key={tab.path}>
<button
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>
</li>
);
})}
<li className="border-t border-base-300 pt-1 mt-1">
<button
onClick={handleConnectOrSyncPS}
className="btn btn-primary btn-sm w-full font-bold mt-1"
>
<RefreshCw className="w-3.5 h-3.5" />
<span>{isConnectPS ? transI18n("sync") : transI18n("connectPs")}</span>
</button>
</li>
</ul>
</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>
{/* Modals */}
{modalConfigs.map(({ id, title, onClose, content }) => (
<dialog key={id} id={id} className="modal">
<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-20 flex items-center justify-between px-6 py-4 border-b border-base-300 bg-base-100">
<h3 className="font-bold text-xl tracking-tight text-primary">
{title}
</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}
>
✕
</button>
</div>
<div className="p-4 sm:p-6 overflow-y-auto max-h-[calc(90vh-5rem)]">
{content}
</div>
</div>
<form method="dialog" className="modal-backdrop">
<button onClick={onClose}>close</button>
</form>
</dialog>
))}
</div>
);
}
+199 -140
View File
@@ -1,140 +1,199 @@
"use client"
import Image from "next/image"
import { useMemo } from "react"
import CharacterCard from "../card/characterCard"
import useLocaleStore from "@/stores/localeStore"
import { useTranslations } from "next-intl"
import useDetailDataStore from "@/stores/detailDataStore"
import useCurrentDataStore from '@/stores/currentDataStore';
import { calcRarity, getNameChar } from "@/helper"
export default function AvatarBar({ onClose }: { onClose?: () => void }) {
const {
avatarSearch,
mapAvatarElementActive,
mapAvatarPathActive,
setAvatarSearch,
setAvatarSelected,
setMapAvatarElementActive,
setMapAvatarPathActive,
setSkillIDSelected,
} = useCurrentDataStore()
const { mapAvatar, baseType, damageType } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const {locale} = useLocaleStore()
const listAvatar = useMemo(() => {
if (!mapAvatar || !locale || !transI18n) return []
let list = Object.values(mapAvatar)
if (avatarSearch) {
list = list.filter(item =>
getNameChar(locale, transI18n, item)
.toLowerCase()
.includes(avatarSearch.toLowerCase())
)
}
const allElementFalse = !Object.values(mapAvatarElementActive).some(v => v)
const allPathFalse = !Object.values(mapAvatarPathActive).some(v => v)
list = list.filter(item =>
(allElementFalse || mapAvatarElementActive[item.DamageType]) &&
(allPathFalse || mapAvatarPathActive[item.BaseType])
)
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return b.ID - a.ID
})
return list
}, [mapAvatar, mapAvatarElementActive, mapAvatarPathActive, avatarSearch, locale, transI18n])
return (
<div className="grid grid-flow-row h-full auto-rows-max w-full">
<div className="h-full rounded-lg mx-2 py-2">
<div className="container">
<div className="flex flex-col h-full gap-2">
<div className="flex flex-col gap-2">
<div className="flex justify-center">
<input type="text"
placeholder={transI18n("placeholderCharacter")}
className="input input-bordered input-primary w-full"
value={avatarSearch}
onChange={(e) => setAvatarSearch(e.target.value)}
/>
</div>
<div className="grid grid-cols-7 sm:grid-cols-4 lg:grid-cols-7 mb-1 mx-1 gap-2 w-full max-h-[17vh] min-h-[5vh] overflow-y-auto">
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setMapAvatarElementActive({ ...mapAvatarElementActive, [key]: !mapAvatarElementActive[key] })
}}
className="hover:bg-gray-600 grid items-center justify-items-center cursor-pointer rounded-md shadow-lg"
style={{
backgroundColor: mapAvatarElementActive[key] ? "#374151" : "#6B7280"
}}>
<Image
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
unoptimized
crossOrigin="anonymous"
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
width={200}
height={200} />
</div>
))}
</div>
<div className="grid grid-cols-9 sm:grid-cols-5 lg:grid-cols-9 mb-1 mx-1 gap-2 overflow-y-auto w-full max-h-[17vh] min-h-[5vh]">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setMapAvatarPathActive({ ...mapAvatarPathActive, [key]: !mapAvatarPathActive[key] })
}}
className="hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-lg cursor-pointer"
style={{
backgroundColor: mapAvatarPathActive[key] ? "#374151" : "#6B7280"
}}
>
<Image
src={`${process.env.CDN_URL}/${value.Icon}`}
unoptimized
crossOrigin="anonymous"
alt={key}
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
width={200}
height={200} />
</div>
))}
</div>
</div>
<div className="flex items-start h-full">
<ul className="grid grid-cols-3 sm:grid-cols-2 lg:grid-cols-3 gap-2 w-full h-[65vh] overflow-y-scroll overflow-x-hidden">
{listAvatar.map((item, index) => (
<div key={index} onClick={() => {
setAvatarSelected(item);
setSkillIDSelected(null)
if (onClose) onClose()
}}>
<CharacterCard data={item} />
</div>
))}
</ul>
</div>
</div>
</div>
</div>
</div>
)
}
"use client"
import Image from "next/image"
import { useMemo } from "react"
import CharacterCard from "../card/characterCard"
import useLocaleStore from "@/stores/localeStore"
import { useTranslations } from "next-intl"
import useDetailDataStore from "@/stores/detailDataStore"
import useCurrentDataStore from '@/stores/currentDataStore';
import { calcRarity, getNameChar } from "@/helper"
import { useShallow } from "zustand/react/shallow";
import { Search, X } from "lucide-react"
export default function AvatarBar({ onClose }: { onClose?: () => void }) {
const {
avatarSearch,
mapAvatarElementActive,
mapAvatarPathActive,
setAvatarSearch,
setAvatarSelected,
setMapAvatarElementActive,
setMapAvatarPathActive,
setSkillIDSelected,
} = useCurrentDataStore(
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 { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const listAvatar = useMemo(() => {
if (!mapAvatar || !locale || !transI18n) return []
let list = Object.values(mapAvatar)
if (avatarSearch) {
list = list.filter(item =>
getNameChar(locale, transI18n, item)
.toLowerCase()
.includes(avatarSearch.toLowerCase())
)
}
const allElementFalse = !Object.values(mapAvatarElementActive).some(v => v)
const allPathFalse = !Object.values(mapAvatarPathActive).some(v => v)
list = list.filter(item =>
(allElementFalse || mapAvatarElementActive[item.DamageType]) &&
(allPathFalse || mapAvatarPathActive[item.BaseType])
)
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return b.ID - a.ID
})
return list
}, [mapAvatar, mapAvatarElementActive, mapAvatarPathActive, avatarSearch, locale, transI18n])
return (
<div className="flex flex-col h-full gap-2 p-2">
{/* Compact Search Bar */}
<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">
<Search className="w-4 h-4 text-base-content/70 shrink-0" />
<input
type="text"
placeholder={transI18n("placeholderCharacter")}
className="grow text-xs bg-transparent border-none outline-none focus:outline-none placeholder:text-base-content/50"
value={avatarSearch}
onChange={(e) => setAvatarSearch(e.target.value)}
/>
{avatarSearch && (
<button
type="button"
onClick={() => setAvatarSearch("")}
className="text-base-content/50 hover:text-base-content"
>
<X className="w-3.5 h-3.5" />
</button>
)}
</label>
{/* Compact Filter Section */}
<div className="p-1 rounded-xl bg-base-200/80 border border-base-content/15 space-y-1 shadow-xs">
{/* Element Filters - 7 Elements on 1 Row (gap-0.5) */}
<div className="grid grid-cols-7 gap-0.5">
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => {
const isActive = !!mapAvatarElementActive[key];
return (
<button
key={key}
type="button"
onClick={() => {
setMapAvatarElementActive({ ...mapAvatarElementActive, [key]: !mapAvatarElementActive[key] })
}}
className={`h-8.5 w-full rounded-lg transition-all duration-150 active-press flex items-center justify-center p-0 ${
isActive
? "bg-primary/20 text-primary ring-1.5 ring-primary scale-105 z-10 shadow-xs"
: "hover:bg-base-100 opacity-90 hover:opacity-100"
}`}
title={value.DamageTypeName || key}
>
<Image
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
unoptimized
crossOrigin="anonymous"
className="w-6.5 h-6.5 object-contain drop-shadow-xs"
width={32}
height={32}
/>
</button>
);
})}
</div>
{/* Path Filters - 9 Paths on 1 Row (gap-0.5) */}
<div className="grid grid-cols-9 gap-0.5">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => {
const isActive = !!mapAvatarPathActive[key];
return (
<button
key={key}
type="button"
onClick={() => {
setMapAvatarPathActive({ ...mapAvatarPathActive, [key]: !mapAvatarPathActive[key] })
}}
className={`h-8 w-full rounded-lg transition-all duration-150 active-press flex items-center justify-center p-0 ${
isActive
? "bg-primary/20 ring-1.5 ring-primary scale-105 z-10 shadow-xs"
: "hover:bg-base-100 opacity-90 hover:opacity-100"
}`}
title={value.BaseTypeName || key}
>
<Image
src={`${process.env.CDN_URL}/${value.Icon}`}
unoptimized
crossOrigin="anonymous"
alt={key}
className={`w-5.5 h-5.5 object-contain ${
isActive ? "theme-path-icon-active" : "theme-path-icon"
}`}
width={28}
height={28}
/>
</button>
);
})}
</div>
</div>
{/* Character Count & Grid List */}
<div className="flex items-center justify-between text-xs font-semibold text-base-content/80 px-1 py-0.5">
<span className="flex items-center gap-1.5">
<span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse"></span>
<span>{transI18n("character")}</span>
</span>
<span className="font-mono text-xs font-bold px-2 py-0.5 rounded-md bg-primary/10 text-primary border border-primary/20">
{listAvatar.length}
</span>
</div>
<div className="flex-1 overflow-y-auto pr-0.5 scrollbar-thin">
<ul className="grid grid-cols-3 sm:grid-cols-2 lg:grid-cols-3 gap-1.5 auto-rows-fr">
{listAvatar.map((item) => (
<div
key={item.ID}
className="h-full flex flex-col"
onClick={() => {
setAvatarSelected(item);
setSkillIDSelected(null)
if (onClose) onClose()
}}
>
<CharacterCard data={item} />
</div>
))}
</ul>
</div>
</div>
)
}
File diff suppressed because it is too large Load Diff
+116 -83
View File
@@ -1,83 +1,116 @@
"use client";
import { getNameChar } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import { AvatarDetail } from '@/types';
import ParseText from '../parseText';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import useDetailDataStore from '@/stores/detailDataStore';
interface CharacterCardProps {
data: AvatarDetail
}
export default function CharacterCard({ data }: CharacterCardProps) {
const { locale } = useLocaleStore();
const transI18n = useTranslations("DataPage");
const { baseType, damageType } = useDetailDataStore()
return (
<li
className="z-10 flex flex-col items-center rounded-xl shadow-xl
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"
>
<div
className={`w-full rounded-md 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-32 lg:h-26 xl:h-36">
<Image
width={376}
height={512}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${data.Image.AvatarIconPath}`}
priority={true}
className="rounded-md w-full h-full object-contain"
alt="ALT"
/>
<Image
width={32}
height={32}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType?.[data.DamageType].Icon}`}
className="absolute top-0 left-0 w-6 h-6 rounded-full"
alt={data.DamageType.toLowerCase()}
/>
<Image
width={32}
height={32}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType?.[data.BaseType].Icon}`}
className="absolute top-0 right-0 w-6 h-6 rounded-full"
alt={data.BaseType.toLowerCase()}
style={{
boxShadow: "inset 0 0 8px 4px #9CA3AF"
}}
/>
</div>
</div>
<ParseText
locale={locale}
text={getNameChar(locale, transI18n, data)}
className="
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
"
/>
</li>
);
}
"use client";
import { getNameChar } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import { AvatarDetail } from '@/types';
import ParseText from '../parseText';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
import useDetailDataStore from '@/stores/detailDataStore';
import { useShallow } from 'zustand/react/shallow';
import useUserDataStore from '@/stores/userDataStore';
interface CharacterCardProps {
data: AvatarDetail
}
export default function CharacterCard({ data }: CharacterCardProps) {
const { locale } = useLocaleStore(useShallow((state) => ({ locale: state.locale })));
const transI18n = useTranslations("DataPage");
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 (
<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"
>
{/* Artwork container with vibrant rarity background */}
<div
className={`w-full relative aspect-[3/4] flex items-center justify-center overflow-hidden bg-linear-to-b ${
is5Star
? "from-yellow-400 via-amber-500/85 to-amber-700/70"
: is4Star
? "from-purple-400 via-purple-600/80 to-purple-800/60"
: "from-sky-400 via-blue-600/80 to-blue-800/60"
}`}
>
<Image
width={376}
height={512}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${data.Image.AvatarIconPath}`}
priority={true}
className="w-full h-full object-contain transition-transform duration-300 ease-out group-hover:scale-105"
alt={data.ID.toString()}
/>
{/* 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
width={28}
height={28}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType[data.DamageType].Icon}`}
className="absolute top-1 left-1 w-6 h-6 object-contain drop-shadow-sm"
alt={data.DamageType.toLowerCase()}
/>
)}
{/* Base Type / Path Badge */}
{baseType?.[data.BaseType]?.Icon && (
<Image
width={28}
height={28}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType[data.BaseType].Icon}`}
className="absolute top-1 right-1 w-6 h-6 object-contain rounded-full drop-shadow-sm"
alt={data.BaseType.toLowerCase()}
style={{
boxShadow: "inset 0 0 6px 3px rgba(255, 255, 255, 0.4)"
}}
/>
)}
</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
locale={locale}
text={getNameChar(locale, transI18n, data)}
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"
/>
</div>
</li>
);
}
+156 -124
View File
@@ -1,125 +1,157 @@
"use client";
import React from 'react';
import { CharacterInfoCardType } from '@/types';
import useLocaleStore from '@/stores/localeStore';
import Image from 'next/image';
import ParseText from '../parseText';
import useDetailDataStore from '@/stores/detailDataStore';
import { getLocaleName } from '@/helper/getName';
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 { mapAvatar, mapLightCone, baseType, damageType } = useDetailDataStore();
const { locale } = useLocaleStore();
return (
<div
className={`bg-base-200/60 rounded-xl p-4 border cursor-pointer transition-all duration-200 ${isSelected
? 'border-blue-400 ring-2 ring-blue-400/50'
: 'border-base-300/50 hover:border-base-300 opacity-75'
}`}
onClick={() => onCharacterToggle(character)}
>
{/* Character Portrait */}
<div className="relative mb-4">
<div className="w-full h-48 rounded-lg overflow-hidden relative">
<Image
src={`${process.env.CDN_URL}/${mapAvatar?.[character.avatar_id.toString()]?.Image?.AvatarIconPath}`}
alt={getLocaleName(locale, mapAvatar?.[character.avatar_id.toString()]?.Name)}
width={376}
height={512}
unoptimized
crossOrigin="anonymous"
className="w-full h-full object-contain"
/>
<Image
width={48}
height={48}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType?.[mapAvatar?.[character.avatar_id.toString()]?.DamageType || ""]?.Icon}`}
className="absolute top-0 left-0 w-10 h-10 rounded-full"
alt={mapAvatar[character.avatar_id.toString()]?.DamageType.toLowerCase()}
/>
<Image
width={48}
height={48}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType?.[mapAvatar?.[character.avatar_id.toString()]?.BaseType || ""]?.Icon}`}
className="absolute top-0 right-0 w-10 h-10 rounded-full"
alt={mapAvatar[character.avatar_id.toString()]?.BaseType.toLowerCase()}
style={{
boxShadow: "inset 0 0 8px 4px #9CA3AF"
}}
/>
</div>
</div>
{/* Character Name and Level */}
<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 && (
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
{character.relics.map((relic, index) => (
<div key={index} className="relative">
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50">
<Image
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"
unoptimized
crossOrigin="anonymous"
width={124}
height={124}
className="w-14 h-14 object-contain"
/>
</div>
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white text-xs px-1 rounded">
+{relic.level}
</div>
</div>
))}
</div>
)}
{/* Light Cone */}
{character.lightcone.item_id && (
<div className="">
<div className="rounded-lg h-42 flex items-center justify-center">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${mapLightCone?.[character.lightcone.item_id.toString()]?.Image?.ImagePath}`}
alt={getLocaleName(locale, mapLightCone?.[character.lightcone.item_id.toString()]?.Name)}
width={348}
height={408}
className="w-full h-full object-contain rounded-lg"
/>
</div>
<div className="w-full h-full rounded-lg flex items-center justify-center">
<div className="text-center">
<div className="text-lg font-bold">
<ParseText
text={getLocaleName(locale, mapLightCone[character.lightcone.item_id.toString()]?.Name)}
locale={locale}
/>
</div>
<div className="text-base mb-1">Lv.{character.lightcone.level} S{character.lightcone.rank}</div>
</div>
</div>
</div>
)}
</div>
);
"use client";
import React from 'react';
import { CharacterInfoCardType } from '@/types';
import useLocaleStore from '@/stores/localeStore';
import Image from 'next/image';
import ParseText from '../parseText';
import useDetailDataStore from '@/stores/detailDataStore';
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
}) {
const isSelected = selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id);
const { mapAvatar, mapLightCone, baseType, damageType } = useDetailDataStore(
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 (
<div
className={`group rounded-2xl p-4 border transition-all duration-200 cursor-pointer select-none ${
isSelected
? '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)}
>
{/* Character Portrait */}
<div className="relative mb-3 aspect-[4/5] rounded-xl overflow-hidden bg-base-300/40 flex items-center justify-center">
{avatar && (
<>
<Image
src={`${process.env.CDN_URL}/${avatar.Image?.AvatarIconPath}`}
alt={getLocaleName(locale, avatar.Name)}
width={376}
height={512}
unoptimized
crossOrigin="anonymous"
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
width={24}
height={24}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType[avatar.DamageType].Icon}`}
className="w-5 h-5 object-contain"
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
width={24}
height={24}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType[avatar.BaseType].Icon}`}
className="w-5 h-5 object-contain"
alt={avatar.BaseType.toLowerCase()}
/>
</div>
)}
</>
)}
</div>
{/* Character Name and Level Badge */}
<div className="text-center mb-3">
<ParseText
className="text-base font-bold line-clamp-1"
text={avatar ? getLocaleName(locale, avatar.Name) : character.avatar_id.toString()}
locale={locale}
/>
<div className="flex items-center justify-center gap-2 mt-1">
<span className="text-xs font-mono font-semibold px-2 py-0.5 rounded-full bg-base-300/80 text-base-content/80">
Lv.{character.level}
</span>
<span className="text-xs font-mono font-bold px-2 py-0.5 rounded-full bg-amber-500/20 text-amber-400 border border-amber-500/30">
E{character.rank}
</span>
</div>
</div>
{/* Relics Mini-grid */}
{character.relics.length > 0 && (
<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) => (
<div key={index} className="relative group/relic">
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-base-200 border border-amber-500/40">
<Image
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"
unoptimized
crossOrigin="anonymous"
width={32}
height={32}
className="w-6 h-6 object-contain"
/>
</div>
<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}
</div>
</div>
))}
</div>
)}
{/* Light Cone */}
{character.lightcone && character.lightcone.item_id !== 0 && (
<div className="p-2 rounded-xl bg-base-100/50 border border-white/5 text-center">
<div className="h-28 flex items-center justify-center mb-1">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${mapLightCone?.[character.lightcone.item_id.toString()]?.Image?.ImagePath}`}
alt="Lightcone"
width={160}
height={200}
className="h-full w-auto object-contain rounded-lg"
/>
</div>
<div className="text-xs font-bold truncate">
<ParseText
text={getLocaleName(locale, mapLightCone?.[character.lightcone.item_id.toString()]?.Name)}
locale={locale}
/>
</div>
<div className="flex items-center justify-center gap-2 mt-1">
<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>
);
};
+55 -53
View File
@@ -1,53 +1,55 @@
"use client";
import { getLocaleName } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import ParseText from '../parseText';
import Image from 'next/image';
import { LightConeDetail } from '@/types';
interface LightconeCardProps {
data: LightConeDetail
}
export default function LightconeCard({ data }: LightconeCardProps) {
const { locale } = useLocaleStore();
const text = getLocaleName(locale, data.Name)
return (
<li className="z-10 flex flex-col items-center rounded-md shadow-lg
bg-linear-to-b from-customStart to-customEnd transform transition-transform duration-300
hover:scale-105 cursor-pointer min-h-55"
>
<div
className={`w-full rounded-md bg-linear-to-br ${data.Rarity === "CombatPowerLightconeRarity5"
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
: data.Rarity === "CombatPowerLightconeRarity4" ? "from-purple-400 via-purple-600/70 to-purple-800/50" :
"from-blue-400 via-blue-600/70 to-blue-800/50"
}`}
>
<div className="relative w-full h-full">
<Image
loading="lazy"
src={`${process.env.CDN_URL}/${data?.Image?.ThumbnailPath}`}
unoptimized
crossOrigin="anonymous"
width={348}
height={408}
className="w-full h-full rounded-md object-cover"
alt="ALT"
/>
</div>
</div>
<ParseText
locale={locale}
text={text}
className="mt-2 px-1 text-center text-sm sm:text-base font-bold leading-tight"
/>
</li>
);
}
"use client";
import { getLocaleName } from '@/helper';
import useLocaleStore from '@/stores/localeStore';
import { useShallow } from 'zustand/react/shallow';
import ParseText from '../parseText';
import Image from 'next/image';
import { LightConeDetail } from '@/types';
interface LightconeCardProps {
data: LightConeDetail
}
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";
return (
<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">
{/* Artwork container with vibrant rarity background */}
<div
className={`w-full relative aspect-[3/4] flex items-center justify-center overflow-hidden bg-linear-to-b ${
is5Star
? "from-yellow-400 via-amber-500/85 to-amber-700/70"
: is4Star
? "from-purple-400 via-purple-600/80 to-purple-800/60"
: "from-sky-400 via-blue-600/80 to-blue-800/60"
}`}
>
<Image
loading="lazy"
src={`${process.env.CDN_URL}/${data?.Image?.ThumbnailPath}`}
unoptimized
crossOrigin="anonymous"
width={348}
height={408}
className="w-full h-full object-cover transition-transform duration-300 ease-out group-hover:scale-105"
alt={data?.Name?.en || "Lightcone"}
/>
</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
locale={locale}
text={text}
className="font-bold text-xs leading-tight line-clamp-2 text-base-content group-hover:text-primary transition-colors text-center"
/>
</div>
</li>
);
}
+89 -78
View File
@@ -1,79 +1,90 @@
"use client";
import React from 'react';
import { AvatarProfileCardType } from '@/types';
import useLocaleStore from '@/stores/localeStore';
import Image from 'next/image';
import ParseText from '../parseText';
import useDetailDataStore from '@/stores/detailDataStore';
import { getLocaleName } from '@/helper/getName';
export default function ProfileCard({ profile, selectedProfile, onProfileToggle }: { profile: AvatarProfileCardType, selectedProfile: AvatarProfileCardType[], onProfileToggle: (profileId: AvatarProfileCardType) => void }) {
const isSelected = selectedProfile.some((selectedProfile) => selectedProfile.key === profile.key);
const { mapLightCone } = useDetailDataStore();
const { locale } = useLocaleStore();
return (
<div
className={`bg-base-200/60 rounded-xl p-4 border cursor-pointer transition-all duration-200 ${isSelected
? 'border-blue-400 ring-2 ring-blue-400/50'
: 'border-base-300/50 hover:border-base-300 opacity-75'
}`}
onClick={() => onProfileToggle(profile)}
>
{/* Light Cone */}
{profile.lightcone && (
<div className="">
<div className="rounded-lg h-42 flex items-center justify-center">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/lightconemaxfigures/${profile.lightcone.item_id}.png`}
alt={getLocaleName(locale, mapLightCone[profile.lightcone.item_id.toString()]?.Name)}
width={348}
height={408}
className="w-full h-full object-contain rounded-lg"
/>
</div>
<div className="w-full h-full rounded-lg flex items-center justify-center">
<div className="text-center">
<div className="text-lg font-bold">
<ParseText
text={getLocaleName(locale, mapLightCone[profile.lightcone.item_id.toString()]?.Name)}
locale={locale}
/>
</div>
<div className="text-base mb-1">Lv.{profile.lightcone.level} S{profile.lightcone.rank}</div>
</div>
</div>
</div>
)}
{Object.keys(profile.relics).length > 0 && (
<div className="flex flex-wrap items-center justify-center gap-2 mb-4">
{Object.values(profile.relics).map((relic, index) => (
<div key={index} className="relative">
<div className="w-9 h-9 rounded-lg flex items-center justify-center border border-amber-500/50">
<Image
unoptimized
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`}
alt="Relic"
width={124}
height={124}
className="w-14 h-14 object-contain"
/>
</div>
<div className="absolute -bottom-2 left-1/2 transform -translate-x-1/2 bg-slate-800 text-white text-xs px-1 rounded">
+{relic.level}
</div>
</div>
))}
</div>
)}
</div>
);
"use client";
import React from 'react';
import { AvatarProfileCardType } from '@/types';
import useLocaleStore from '@/stores/localeStore';
import Image from 'next/image';
import ParseText from '../parseText';
import useDetailDataStore from '@/stores/detailDataStore';
import { getLocaleName } from '@/helper/getName';
import { useShallow } from 'zustand/react/shallow';
export default function ProfileCard({
profile,
selectedProfile,
onProfileToggle
}: {
profile: AvatarProfileCardType,
selectedProfile: AvatarProfileCardType[],
onProfileToggle: (profileId: AvatarProfileCardType) => void
}) {
const isSelected = selectedProfile.some((selectedProfile) => selectedProfile.key === profile.key);
const { mapLightCone } = useDetailDataStore(
useShallow((state) => ({ mapLightCone: state.mapLightCone }))
);
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
);
return (
<div
className={`group rounded-2xl p-4 border transition-all duration-200 cursor-pointer select-none ${
isSelected
? '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)}
>
{/* Light Cone */}
{profile.lightcone && (
<div className="p-2 rounded-xl bg-base-100 border border-base-300 text-center mb-3">
<div className="h-32 flex items-center justify-center mb-2">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/lightconemaxfigures/${profile.lightcone.item_id}.png`}
alt="Lightcone"
width={240}
height={300}
className="h-full w-auto object-contain rounded-lg transition-transform duration-200 group-hover:scale-105"
/>
</div>
<div className="text-sm font-bold truncate">
<ParseText
text={getLocaleName(locale, mapLightCone?.[profile.lightcone.item_id.toString()]?.Name)}
locale={locale}
/>
</div>
<div className="flex items-center justify-center gap-2 mt-1">
<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>
)}
{/* Relics Mini-grid */}
{Object.keys(profile.relics).length > 0 && (
<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) => (
<div key={index} className="relative group/relic">
<div className="w-8 h-8 rounded-lg flex items-center justify-center bg-base-200 border border-amber-500/40">
<Image
unoptimized
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`}
alt="Relic"
width={32}
height={32}
className="w-6 h-6 object-contain"
/>
</div>
<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}
</div>
</div>
))}
</div>
)}
</div>
);
};
+134 -195
View File
@@ -1,196 +1,135 @@
"use client";
import useRelicMakerStore from "@/stores/relicMakerStore";
import useUserDataStore from "@/stores/userDataStore";
import Image from "next/image";
import { useMemo } from "react";
interface RelicCardProps {
slot: string
avatarId: string
}
const getRarityColor = (rarity: string) => {
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 '4': return 'border-blue-500 shadow-blue-500/50 bg-linear-to-br from-blue-700 via-blue-400 to-blue-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 '6': return 'border-yellow-500 shadow-yellow-500/50 bg-linear-to-br from-yellow-700 via-yellow-400 to-yellow-500';
default: return 'border-gray-500 shadow-gray-500/50';
}
};
const getRarityName = (slot: string) => {
switch (slot) {
case '1': return (
<div className="flex items-center gap-1">
<Image
unoptimized
crossOrigin="anonymous"
src="/relics/HEAD.png"
alt="Head"
width={20}
height={20}
className="bg-black/50 rounded-full"
/>
<h2>Head</h2>
</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) {
const { avatars } = useUserDataStore()
const { selectedRelicSlot } = useRelicMakerStore()
const relicDetail = useMemo(() => {
const avatar = avatars[avatarId];
if (avatar) {
if (avatar.profileList[avatar.profileSelect].relics[slot]) {
return avatar.profileList[avatar.profileSelect].relics[slot];
}
return null;
}
return null;
}, [avatars, avatarId, slot]);
return (
<div>
{relicDetail ? (
<div
className="flex flex-col items-center cursor-pointer ">
<div
className={`
relative w-24 h-24 rounded-full
${getRarityColor(relicDetail.relic_id.toString()[0])}
shadow-xl
flex items-center justify-center
cursor-pointer transition-transform
${selectedRelicSlot === slot ? 'ring-5 ring-success scale-105' : 'ring-3 ring-primary'}
`}
>
<span>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relicDetail.relic_set_id}_${slot}.png`}
alt="Relic"
width={124}
height={124}
className="w-14 h-14 object-contain"
/>
</span>
{/* Level Badge */}
<div className="absolute -bottom-2 bg-base-100 border-2 border-base-300 rounded-full px-2 py-1">
<span className="text-sm font-bold text-primary">+{relicDetail.level}</span>
</div>
</div>
<div className="mt-3 text-center">
<div className="text-sm font-medium text-base-content">{getRarityName(slot)}</div>
</div>
</div>
) : (
<div
className="flex flex-col items-center cursor-pointer">
<div
className={`
relative w-24 h-24 rounded-full border-4
${getRarityColor("None")}
bg-base-300 shadow-xl
flex items-center justify-center
cursor-pointer hover:scale-105 transition-transform
ring-4 ring-primary
`}
>
<span className="text-3xl">
<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>
</span>
{/* Level Badge */}
<div className="absolute -bottom-2 bg-base-100 border-2 border-base-300 rounded-full px-2 py-1">
<span className="text-sm font-bold text-primary">+{0}</span>
</div>
</div>
<div className="mt-3 text-center">
<div>{getRarityName(slot)}</div>
</div>
</div>
)}
</div>
)
"use client";
import useRelicMakerStore from "@/stores/relicMakerStore";
import useUserDataStore from "@/stores/userDataStore";
import { useShallow } from "zustand/react/shallow";
import Image from "next/image";
import { useMemo } from "react";
interface RelicCardProps {
slot: string
avatarId: string
}
const getRarityColor = (rarity: string) => {
switch (rarity) {
case '6':
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-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';
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 getSlotIconAndName = (slot: string) => {
const slotMap: Record<string, { icon: string; name: string }> = {
'1': { icon: '/relics/HEAD.png', name: 'Head' },
'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
unoptimized
crossOrigin="anonymous"
src={current.icon}
alt={current.name}
width={18}
height={18}
className="w-4 h-4 object-contain theme-stat-icon opacity-85"
/>
<span className="text-xs font-bold text-base-content">{current.name}</span>
</div>
);
};
export default function RelicCard({ slot, avatarId }: RelicCardProps) {
const { avatars } = useUserDataStore(
useShallow((state) => ({ avatars: state.avatars }))
)
const { selectedRelicSlot } = useRelicMakerStore(
useShallow((state) => ({ selectedRelicSlot: state.selectedRelicSlot }))
)
const relicDetail = useMemo(() => {
const avatar = avatars[avatarId];
if (avatar) {
const profile = avatar.profileList?.[avatar.profileSelect] ?? avatar.profileList?.[0];
return profile?.relics?.[slot] || null;
}
return null;
}, [avatars, avatarId, slot]);
const isSelected = selectedRelicSlot === slot;
return (
<div className="flex flex-col items-center select-none group cursor-pointer">
{relicDetail ? (
<div className="flex flex-col items-center">
<div
className={`
relative w-22 h-22 sm:w-24 sm:h-24 rounded-full border-3
${getRarityColor(relicDetail.relic_id.toString()[0])}
flex items-center justify-center
transition-all duration-200
${isSelected ? 'ring-2 ring-primary/70 shadow-md ring-offset-2 ring-offset-base-100' : 'hover:border-primary/60'}
`}
>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${relicDetail.relic_set_id}_${slot}.png`}
alt="Relic"
width={124}
height={124}
className="w-14 h-14 sm:w-16 sm:h-16 object-contain drop-shadow-md"
/>
{/* Level Badge */}
<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-xs font-mono font-bold text-primary">+{relicDetail.level}</span>
</div>
</div>
{getSlotIconAndName(slot)}
</div>
) : (
<div className="flex flex-col items-center">
<div
className={`
relative w-22 h-22 sm:w-24 sm:h-24 rounded-full border-2 border-dashed border-base-content/20
bg-base-200/60 flex items-center justify-center
transition-all duration-200 group-hover:border-primary/60
${isSelected ? 'ring-2 ring-primary/70 border-solid bg-primary/10 ring-offset-2 ring-offset-base-100' : ''}
`}
>
<div className="flex flex-col items-center justify-center gap-1 text-base-content/40 group-hover:text-primary transition-colors">
<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" />
</svg>
</div>
{/* Level Badge */}
<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-xs font-mono font-bold text-base-content/40">+0</span>
</div>
</div>
{getSlotIconAndName(slot)}
</div>
)}
</div>
)
}
+83 -68
View File
@@ -1,69 +1,84 @@
"use client";
import Image from 'next/image';
import { AvatarDetail } from '@/types';
import useDetailDataStore from '@/stores/detailDataStore';
interface SimpleAvatarCardProps {
data: AvatarDetail;
isSelected?: boolean;
onClick?: () => void;
showRemoveHover?: boolean;
}
export const SimpleAvatarCard = ({ data, isSelected, onClick, showRemoveHover }: SimpleAvatarCardProps) => {
const { baseType, damageType } = useDetailDataStore()
return (
<div
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
${isSelected ? 'ring-2 ring-success opacity-60' : ''}
bg-linear-to-br ${data.Rarity === "CombatPowerAvatarRarityType5"
? "from-yellow-400 via-yellow-600/70 to-yellow-800/50"
: "from-purple-400 via-purple-600/70 to-purple-800/50"
}`}
>
<Image
width={80}
height={80}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${data.Image.ActionAvatarHeadIconPath}`}
priority={true}
className="w-full h-full object-contain"
alt="Avatar"
/>
<div className="absolute top-0 left-0 w-5 h-5 bg-black/40 rounded-full flex items-center justify-center p-0.5">
<Image
width={20}
height={20}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType?.[data.DamageType]?.Icon}`}
className="w-full h-full object-contain"
alt="Element"
/>
</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">
<Image
width={20}
height={20}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType?.[data.BaseType]?.Icon}`}
className="w-full h-full object-contain"
alt="Path"
/>
</div>
{showRemoveHover && (
<div className="absolute inset-0 bg-error/80 rounded-md flex items-center justify-center opacity-0 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">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</div>
)}
</div>
);
"use client";
import Image from 'next/image';
import { AvatarDetail } from '@/types';
import useDetailDataStore from '@/stores/detailDataStore';
import { useShallow } from 'zustand/react/shallow';
import { X } from 'lucide-react';
interface SimpleAvatarCardProps {
data: AvatarDetail;
isSelected?: boolean;
onClick?: () => void;
showRemoveHover?: boolean;
}
export const SimpleAvatarCard = ({ data, isSelected, onClick, showRemoveHover }: SimpleAvatarCardProps) => {
const { baseType, damageType } = useDetailDataStore(
useShallow((state) => ({
baseType: state.baseType,
damageType: state.damageType,
}))
)
const is5Star = data.Rarity === "CombatPowerAvatarRarityType5";
return (
<div
onClick={onClick}
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-primary opacity-60 scale-95' : ''
}`}
>
<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
width={80}
height={80}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${data.Image.ActionAvatarHeadIconPath}`}
priority={true}
className="w-full h-full object-contain transition-transform duration-200 group-hover:scale-105"
alt="Avatar"
/>
{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
width={20}
height={20}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType[data.DamageType].Icon}`}
className="w-full h-full object-contain"
alt="Element"
/>
</div>
)}
{baseType?.[data.BaseType]?.Icon && (
<div className="absolute top-0.5 right-0.5 w-4 h-4 sm:w-5 sm:h-5 bg-black/60 backdrop-blur-xs rounded-full flex items-center justify-center p-0.5 border border-white/10">
<Image
width={20}
height={20}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${baseType[data.BaseType].Icon}`}
className="w-full h-full object-contain"
alt="Path"
/>
</div>
)}
{showRemoveHover && (
<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">
<X className="h-6 w-6 text-white" />
</div>
)}
</div>
</div>
);
};
+85 -90
View File
@@ -1,91 +1,86 @@
import useLocaleStore from '@/stores/localeStore';
import { Check, Sparkles, Bug, Zap, Package, Calendar } from 'lucide-react';
export default function ChangelogBar() {
const { changelog } = useLocaleStore()
const getIcon = (type: string) => {
switch (type) {
case 'feature':
return <Sparkles className="w-3 h-3 md:w-4 md:h-4" />;
case 'fix':
return <Bug className="w-3 h-3 md:w-4 md:h-4" />;
case 'improvement':
return <Zap className="w-3 h-3 md:w-4 md:h-4" />;
default:
return <Package className="w-3 h-3 md:w-4 md:h-4" />;
}
};
const getBadgeClass = (type: string) => {
switch (type) {
case 'feature':
return 'badge-success';
case 'fix':
return 'badge-error';
case 'improvement':
return 'badge-warning';
default:
return 'badge-info';
}
};
const getTypeLabel = (type: string) => {
switch (type) {
case 'feature':
return 'Feature';
case 'fix':
return 'Fix';
case 'improvement':
return 'Improvement';
default:
return 'Update';
}
};
return (
<div className="md:p-4">
{/* Alert */}
{/* <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) => (
<div key={index} className="bg-base-100 shadow-sm">
<div className="flex flex-col gap-2">
{/* Version Header */}
<div className="flex flex-row items-center gap-2 md:gap-3">
<div className={`badge ${getBadgeClass(change.type)} gap-1 md:gap-2 p-1`}>
{getIcon(change.type)}
{getTypeLabel(change.type)}
</div>
<h2 className="card-title text-sm md:text-lg">
Version {change.version}
</h2>
<div className="flex items-center gap-1 md:gap-2 text-base-content/60 ml-auto">
<Calendar className="w-3 h-3 md:w-4 md:h-4" />
<span className="text-sm">{change.date}</span>
</div>
</div>
{/* Changes List */}
<ul className="space-y-3">
{change.items.map((item, itemIndex) => (
<li key={itemIndex} className="flex items-start gap-3">
<div className="mt-1">
<Check className="w-5 h-5 text-success" />
</div>
<span className="text-base-content">{item}</span>
</li>
))}
</ul>
</div>
</div>
))}
</div>
</div>
);
import useLocaleStore from '@/stores/localeStore';
import { useShallow } from 'zustand/react/shallow';
import { Sparkles, Bug, Zap, Package, Calendar } from 'lucide-react';
export default function ChangelogBar() {
const { changelog } = useLocaleStore(
useShallow((state) => ({ changelog: state.changelog }))
)
const getIcon = (type: string) => {
switch (type) {
case 'feature':
return <Sparkles className="w-3.5 h-3.5" />;
case 'fix':
return <Bug className="w-3.5 h-3.5" />;
case 'improvement':
return <Zap className="w-3.5 h-3.5" />;
default:
return <Package className="w-3.5 h-3.5" />;
}
};
const getBadgeClass = (type: string) => {
switch (type) {
case 'feature':
return 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30';
case 'fix':
return 'bg-rose-500/15 text-rose-400 border-rose-500/30';
case 'improvement':
return 'bg-amber-500/15 text-amber-400 border-amber-500/30';
default:
return 'bg-primary/15 text-primary border-primary/30';
}
};
const getTypeLabel = (type: string) => {
switch (type) {
case 'feature':
return 'Feature';
case 'fix':
return 'Fix';
case 'improvement':
return 'Improvement';
default:
return 'Update';
}
};
return (
<div className="space-y-4">
<div className="space-y-3">
{changelog.map((change, index) => (
<div key={index} className="p-4 sm:p-5 rounded-2xl bg-base-200/60 border border-white/10 shadow-lg space-y-3">
{/* Version Header */}
<div className="flex flex-wrap items-center justify-between gap-2 pb-2.5 border-b border-base-content/10">
<div className="flex 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)}
{getTypeLabel(change.type)}
</span>
<h3 className="font-extrabold text-sm sm:text-base tracking-tight">
Version {change.version}
</h3>
</div>
<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>
{/* Changes List */}
<ul className="space-y-2 pt-1 text-xs sm:text-sm text-base-content/85">
{change.items.map((item, itemIndex) => (
<li key={itemIndex} className="flex items-start gap-2.5">
<div className="mt-1 w-1.5 h-1.5 rounded-full bg-primary shrink-0" />
<span className="leading-relaxed">{item}</span>
</li>
))}
</ul>
</div>
))}
</div>
</div>
);
}
+55 -55
View File
@@ -1,56 +1,56 @@
"use client"
import {
useFetchASGroupData,
useFetchAvatarData,
useFetchChangelog,
useFetchConfigData,
useFetchLightconeData,
useFetchMOCGroupData,
useFetchMonsterData,
useFetchPeakGroupData,
useFetchPFGroupData,
useFetchRelicSetData
} from "@/lib/hooks"
export default function ClientDataFetcher({
children
}: {
children: React.ReactNode
}) {
const q1 = useFetchConfigData()
const q2 = useFetchAvatarData()
const q3 = useFetchLightconeData()
const q4 = useFetchRelicSetData()
const q5 = useFetchMonsterData()
const q6 = useFetchPFGroupData()
const q7 = useFetchMOCGroupData()
const q8 = useFetchASGroupData()
const q9 = useFetchPeakGroupData()
const q10 = useFetchChangelog()
const queries = [q1,q2,q3,q4,q5,q6,q7,q8,q9,q10]
const loading = queries.some(q => q.isLoading)
const progress =
(queries.filter(q => q.isSuccess).length / queries.length) * 100
if (loading) {
return (
<div className="flex h-screen flex-col items-center justify-center gap-4">
<div className="text-lg font-semibold">Loading data...</div>
<progress
className="progress progress-primary w-56"
value={progress}
max="100"
/>
<div>{Math.floor(progress)}%</div>
</div>
)
}
return <>{children}</>
"use client"
import {
useFetchASGroupData,
useFetchAvatarData,
useFetchChangelog,
useFetchConfigData,
useFetchLightconeData,
useFetchMOCGroupData,
useFetchMonsterData,
useFetchPeakGroupData,
useFetchPFGroupData,
useFetchRelicSetData
} from "@/lib/hooks"
export default function ClientDataFetcher({
children
}: {
children: React.ReactNode
}) {
const q1 = useFetchConfigData()
const q2 = useFetchAvatarData()
const q3 = useFetchLightconeData()
const q4 = useFetchRelicSetData()
const q5 = useFetchMonsterData()
const q6 = useFetchPFGroupData()
const q7 = useFetchMOCGroupData()
const q8 = useFetchASGroupData()
const q9 = useFetchPeakGroupData()
const q10 = useFetchChangelog()
const queries = [q1,q2,q3,q4,q5,q6,q7,q8,q9,q10]
const loading = queries.some(q => q.isLoading)
const progress =
(queries.filter(q => q.isSuccess).length / queries.length) * 100
if (loading) {
return (
<div className="flex h-screen flex-col items-center justify-center gap-4">
<div className="text-lg font-semibold">Loading data...</div>
<progress
className="progress progress-primary w-56"
value={progress}
max="100"
/>
<div>{Math.floor(progress)}%</div>
</div>
)
}
return <>{children}</>
}
+202 -168
View File
@@ -1,169 +1,203 @@
"use client"
import { connectToPS, syncDataToPS } from "@/helper"
import useConnectStore from "@/stores/connectStore"
import useGlobalStore from "@/stores/globalStore"
import { PSConnectType } from "@/types"
import { useTranslations } from "next-intl"
import { useState } from "react"
export default function ConnectBar() {
const transI18n = useTranslations("DataPage")
const [message, setMessage] = useState({ text: '', type: '' });
const {
connectionType,
privateType,
serverUrl,
username,
password,
setConnectionType,
setPrivateType,
setServerUrl,
setUsername,
setPassword
} = useConnectStore()
const { isConnectPS, setIsConnectPS } = useGlobalStore()
return (
<div className="px-6 py-4">
<div className="form-control grid grid-cols-1 w-full mb-6">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("connectionType")}</span>
</label>
<select
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={connectionType}
onChange={(e) => {
setIsConnectPS(false)
setConnectionType(e.target.value)
}}
>
<option value={PSConnectType.FireflyGo}>FireflyGo</option>
<option value={PSConnectType.RobinSR}>RobinSR</option>
<option value={PSConnectType.Other}>{transI18n("other")}</option>
</select>
</div>
{connectionType === PSConnectType.Other && (
<div className="flex flex-col md:space-x-4 mb-6 gap-2">
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("serverUrl")}</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderServerUrl")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("privateType")}</span>
</label>
<select
className="select w-full select-bordered border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={privateType}
onChange={(e) => setPrivateType(e.target.value)}
>
<option value="Local">{transI18n("local")}</option>
<option value="Server">{transI18n("server")}</option>
</select>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("username")}</span>
</label>
<input
type="text"
placeholder={transI18n("placeholderUsername")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="form-control w-full mb-4 md:mb-0">
<label className="label">
<span className="label-text font-semibold text-purple-300">{transI18n("password")}</span>
</label>
<input
type="password"
placeholder={transI18n("placeholderPassword")}
className="input input-bordered w-full border-purple-500/30 focus:border-purple-500 bg-base-200 mt-1"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
)}
{message.text && (
<div className={`alert ${message.type === 'success' ? 'alert-success' :
message.type === 'error' ? 'alert-error' : 'alert-info'
} mb-6`}>
<span>{message.text}</span>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6 mb-2">
<div className="flex items-center justify-center md:justify-start">
<span className="text-md mr-2">{transI18n("status")}:</span>
<span
className={`badge ${isConnectPS ? "badge-success" : "badge-error"
} badge-lg`}
>
{isConnectPS ? transI18n("connected") : transI18n("unconnected")}
</span>
</div>
{/* Buttons */}
<div className="flex flex-col sm:flex-row gap-2 w-full justify-center md:justify-end">
<button
onClick={async () => {
const response = await connectToPS();
if (response.success) {
setMessage({
type: "success",
text: transI18n("connectedSuccess"),
});
} else {
setMessage({
type: "error",
text: response.message,
});
}
}}
className="btn btn-primary w-full sm:w-auto"
>
{transI18n("connectPs")}
</button>
{isConnectPS && (
<button
onClick={async () => {
const response = await syncDataToPS();
if (response.success) {
setMessage({
type: "success",
text: transI18n("syncSuccess"),
});
} else {
setMessage({
type: "error",
text: `${transI18n("syncFailed")}: ${response.message}`,
});
}
}}
className="btn btn-success w-full sm:w-auto"
>
{transI18n("sync")}
</button>
)}
</div>
</div>
</div>
)
"use client"
import { connectToPS, syncDataToPS } from "@/helper"
import useConnectStore from "@/stores/connectStore"
import useGlobalStore from "@/stores/globalStore"
import { PSConnectType } from "@/types"
import { useTranslations } from "next-intl"
import { useState } from "react"
import { useShallow } from "zustand/react/shallow"
import { Server, RefreshCw, AlertCircle } from "lucide-react"
export default function ConnectBar() {
const transI18n = useTranslations("DataPage")
const [message, setMessage] = useState({ text: '', type: '' });
const {
connectionType,
privateType,
serverUrl,
username,
password,
setConnectionType,
setPrivateType,
setServerUrl,
setUsername,
setPassword
} = useConnectStore(
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 (
<div className="space-y-4">
<div className="p-4 sm:p-6 rounded-2xl bg-base-200/60 border border-white/10 space-y-4">
{/* Connection Type Selection */}
<div className="space-y-1.5">
<label className="block text-xs font-bold uppercase tracking-wider text-base-content/70">
{transI18n("connectionType")}
</label>
<select
className="select select-bordered w-full rounded-xl bg-base-100 border-base-content/10 focus:border-primary text-sm font-medium"
value={connectionType}
onChange={(e) => {
setIsConnectPS(false)
setConnectionType(e.target.value)
}}
>
<option value={PSConnectType.FireflyGo}>FireflyGo</option>
<option value={PSConnectType.RobinSR}>RobinSR</option>
<option value={PSConnectType.Other}>{transI18n("other")}</option>
</select>
</div>
{connectionType === PSConnectType.Other && (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-2">
<div className="space-y-1">
<label className="block text-xs font-semibold text-base-content/70">
{transI18n("serverUrl")}
</label>
<input
type="text"
placeholder={transI18n("placeholderServerUrl")}
className="input input-sm sm:input-md input-bordered w-full rounded-xl bg-base-100 border-base-content/10 focus:border-primary text-xs sm:text-sm"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
</div>
<div className="space-y-1">
<label className="block text-xs font-semibold text-base-content/70">
{transI18n("privateType")}
</label>
<select
className="select select-sm sm:select-md select-bordered w-full rounded-xl bg-base-100 border-base-content/10 focus:border-primary text-xs sm:text-sm"
value={privateType}
onChange={(e) => setPrivateType(e.target.value)}
>
<option value="Local">{transI18n("local")}</option>
<option value="Server">{transI18n("server")}</option>
</select>
</div>
<div className="space-y-1">
<label className="block text-xs font-semibold text-base-content/70">
{transI18n("username")}
</label>
<input
type="text"
placeholder={transI18n("placeholderUsername")}
className="input input-sm sm:input-md input-bordered w-full rounded-xl bg-base-100 border-base-content/10 focus:border-primary text-xs sm:text-sm"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="space-y-1">
<label className="block text-xs font-semibold text-base-content/70">
{transI18n("password")}
</label>
<input
type="password"
placeholder={transI18n("placeholderPassword")}
className="input input-sm sm:input-md input-bordered w-full rounded-xl bg-base-100 border-base-content/10 focus:border-primary text-xs sm:text-sm"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
</div>
)}
{/* Status Message Notification */}
{message.text && (
<div className={`p-3 rounded-xl border text-xs flex items-center gap-2 ${
message.type === 'success'
? 'bg-emerald-500/10 border-emerald-500/30 text-emerald-400'
: message.type === 'error'
? 'bg-rose-500/10 border-rose-500/30 text-rose-400'
: 'bg-info/10 border-info/30 text-info'
}`}>
<AlertCircle className="w-4 h-4 shrink-0" />
<span>{message.text}</span>
</div>
)}
{/* Footer Controls */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 pt-3 border-t border-base-content/10">
<div className="flex items-center gap-2">
<span className="text-xs text-base-content/60">{transI18n("status")}:</span>
<div className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-mono font-bold border ${
isConnectPS
? "bg-emerald-500/15 border-emerald-500/30 text-emerald-400"
: "bg-rose-500/15 border-rose-500/30 text-rose-400"
}`}>
<span className={`w-2 h-2 rounded-full ${isConnectPS ? 'bg-emerald-400 animate-pulse' : 'bg-rose-400'}`}></span>
<span>{isConnectPS ? transI18n("connected") : transI18n("unconnected")}</span>
</div>
</div>
<div className="flex items-center gap-2 w-full sm:w-auto">
<button
onClick={async () => {
const response = await connectToPS();
if (response.success) {
setMessage({
type: "success",
text: transI18n("connectedSuccess"),
});
} else {
setMessage({
type: "error",
text: response.message,
});
}
}}
className="btn btn-sm btn-primary rounded-xl font-bold flex-1 sm:flex-none shadow-xs"
>
<Server className="w-3.5 h-3.5" />
{transI18n("connectPs")}
</button>
{isConnectPS && (
<button
onClick={async () => {
const response = await syncDataToPS();
if (response.success) {
setMessage({
type: "success",
text: transI18n("syncSuccess"),
});
} else {
setMessage({
type: "error",
text: `${transI18n("syncFailed")}: ${response.message}`,
});
}
}}
className="btn btn-sm btn-accent rounded-xl font-bold flex-1 sm:flex-none shadow-xs"
>
<RefreshCw className="w-3.5 h-3.5" />
{transI18n("sync")}
</button>
)}
</div>
</div>
</div>
</div>
)
}
+142 -85
View File
@@ -1,85 +1,142 @@
"use client"
import { replaceByParam, getLocaleName } from '@/helper';
import Image from "next/image";
import ParseText from "../parseText";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import { useMemo } from "react";
import { useTranslations } from "next-intl";
import useCurrentDataStore from "@/stores/currentDataStore";
import ExtraEffectList from '../extraInfo';
export default function EidolonsInfo() {
const { avatarSelected } = useCurrentDataStore()
const { locale } = useLocaleStore()
const transI18n = useTranslations("DataPage")
const { setAvatars, avatars } = useUserDataStore()
const charRank = useMemo(() => {
if (!avatarSelected) return null;
const avatar = avatars[avatarSelected.ID];
if (avatar?.enhanced != "") {
return avatarSelected?.Enhanced?.[avatar?.enhanced]?.Ranks
}
return avatarSelected?.Ranks
}, [avatarSelected, avatars]);
return (
<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"
>
<div
className="cursor-pointer"
onClick={() => {
let newRank = Number(rank.Rank)
if (avatars[avatarSelected?.ID || ""]?.data?.rank == Number(rank.Rank)) {
newRank = Number(rank.Rank) - 1
}
setAvatars({ ...avatars, [avatarSelected?.ID || ""]: { ...avatars[avatarSelected?.ID || ""], data: { ...avatars[avatarSelected?.ID || ""].data, rank: newRank } } })
}}
>
<Image
className={`w-60 object-contain mb-2 ${Number(rank.Rank) <= avatars[avatarSelected?.ID.toString() || ""]?.data?.rank ? "" : "grayscale"}`}
src={`${process.env.CDN_URL}/ui/ui3d/rank/_dependencies/textures/${avatarSelected?.ID}/${avatarSelected?.ID}_Rank_${rank.Rank}.png`}
alt={`Rank ${rank.Rank} Image`}
priority
unoptimized
crossOrigin="anonymous"
width={240}
height={240}
/>
<div className="text-lg pb-1 flex items-center justify-items-center gap-2">
<span className="inline-block text-indigo-500">{rank.Rank}.</span>
<ParseText
locale={locale}
text={getLocaleName(locale, rank.Name)}
className="text-center text-base font-normal leading-tight"
/>
</div>
</div>
<div className="text-sm font-normal">
<div dangerouslySetInnerHTML={{ __html: replaceByParam(getLocaleName(locale, rank.Desc), rank.Param) }} />
<ExtraEffectList extras={rank?.Extra} locale={locale} />
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
"use client"
import { replaceByParam, getLocaleName } from '@/helper';
import Image from "next/image";
import ParseText from "../parseText";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import { useMemo } from "react";
import { useTranslations } from "next-intl";
import useCurrentDataStore from "@/stores/currentDataStore";
import ExtraEffectList from '../extraInfo';
import { useShallow } from "zustand/react/shallow";
export default function EidolonsInfo() {
const { avatarSelected } = useCurrentDataStore(
useShallow((state) => ({ avatarSelected: state.avatarSelected }))
)
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const transI18n = useTranslations("DataPage")
const { setAvatars, avatars } = useUserDataStore(
useShallow((state) => ({
setAvatars: state.setAvatars,
avatars: state.avatars,
}))
)
const charRank = useMemo(() => {
if (!avatarSelected) return null;
const avatar = avatars[avatarSelected.ID];
if (avatar?.enhanced != "") {
return avatarSelected?.Enhanced?.[avatar?.enhanced]?.Ranks
}
return avatarSelected?.Ranks
}, [avatarSelected, avatars]);
const currentRank = avatars[avatarSelected?.ID.toString() || ""]?.data?.rank || 0;
const handleToggleRank = (rankNum: number) => {
if (!avatarSelected) return;
let newRank = rankNum;
if (currentRank === rankNum) {
newRank = rankNum - 1;
}
setAvatars({
...avatars,
[avatarSelected.ID]: {
...avatars[avatarSelected.ID],
data: {
...avatars[avatarSelected.ID]?.data,
rank: newRank,
},
},
});
};
return (
<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
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`}
alt={`Rank ${rank.Rank} Image`}
priority
unoptimized
crossOrigin="anonymous"
width={240}
height={240}
/>
</div>
{/* 2. Eidolon Name & Rank */}
<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
locale={locale}
text={getLocaleName(locale, rank.Name)}
className="font-bold text-base text-base-content group-hover:text-primary transition-colors"
/>
</div>
{/* 3. Description & Extra Effects (Direct readable text, fully selectable & copyable) */}
<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} />
</div>
</div>
);
})}
</div>
)}
</div>
</div>
);
}
+98 -98
View File
@@ -1,99 +1,99 @@
"use client"
import { useState } from "react"
import { replaceByParam, getLocaleName } from "@/helper"
import { ExtraEffect } from "@/types"
import { useTranslations } from "next-intl"
type Props = {
extras: Record<string, ExtraEffect> | undefined
locale: string
}
export default function ExtraEffectList({ extras, locale }: Props) {
const [openList, setOpenList] = useState(false)
const [openId, setOpenId] = useState<number | null>(null)
const transI18n = useTranslations("DataPage")
if (!extras || Object.keys(extras).length === 0) return null
return (
<div className="mt-3">
<div
className="flex items-center justify-between cursor-pointer bg-primary/10 px-3 py-2 rounded-md"
onClick={() => setOpenList(!openList)}
>
<span className="text-sm font-semibold text-primary">
{transI18n("listExtraEffect")} ({Object.keys(extras).length})
</span>
<span
className={`transition-transform ${
openList ? "rotate-90" : ""
}`}
>
▶
</span>
</div>
<div
className={`overflow-hidden transition-all duration-300 ${
openList ? "max-h-125 mt-2" : "max-h-0"
}`}
>
<div className="flex flex-col gap-2">
{Object.values(extras).map((extra) => {
const isOpen = openId === extra.ID
return (
<div
key={extra.ID}
className="bg-primary/5 rounded-md"
>
<div
className="flex items-center justify-between px-3 py-2 cursor-pointer"
onClick={() =>
setOpenId(isOpen ? null : extra.ID)
}
>
<div className="flex items-center gap-2">
<span className="text-[10px] uppercase font-bold bg-primary/50 px-1.5 py-0.5 rounded">
{transI18n("extra")}
</span>
<span className="text-sm font-medium text-primary/80">
{getLocaleName(locale, extra.Name)}
</span>
</div>
<span
className={`transition-transform ${
isOpen ? "rotate-90" : ""
}`}
>
▶
</span>
</div>
<div
className={`overflow-hidden transition-all duration-300 ${
isOpen ? "max-h-40 px-3 pb-2" : "max-h-0"
}`}
>
<div
className="text-sm opacity-90"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, extra.Desc),
extra.Param
)
}}
/>
</div>
</div>
)
})}
</div>
</div>
</div>
)
"use client"
import { useState } from "react"
import { replaceByParam, getLocaleName } from "@/helper"
import { ExtraEffect } from "@/types"
import { useTranslations } from "next-intl"
type Props = {
extras: Record<string, ExtraEffect> | undefined
locale: string
}
export default function ExtraEffectList({ extras, locale }: Props) {
const [openList, setOpenList] = useState(false)
const [openId, setOpenId] = useState<number | null>(null)
const transI18n = useTranslations("DataPage")
if (!extras || Object.keys(extras).length === 0) return null
return (
<div className="mt-3">
<div
className="flex items-center justify-between cursor-pointer bg-primary/10 px-3 py-2 rounded-md"
onClick={() => setOpenList(!openList)}
>
<span className="text-sm font-semibold text-primary">
{transI18n("listExtraEffect")} ({Object.keys(extras).length})
</span>
<span
className={`transition-transform ${
openList ? "rotate-90" : ""
}`}
>
▶
</span>
</div>
<div
className={`overflow-hidden transition-all duration-300 ${
openList ? "max-h-125 mt-2" : "max-h-0"
}`}
>
<div className="flex flex-col gap-2">
{Object.values(extras).map((extra) => {
const isOpen = openId === extra.ID
return (
<div
key={extra.ID}
className="bg-primary/5 rounded-md"
>
<div
className="flex items-center justify-between px-3 py-2 cursor-pointer"
onClick={() =>
setOpenId(isOpen ? null : extra.ID)
}
>
<div className="flex items-center gap-2">
<span className="text-[10px] uppercase font-bold bg-primary/50 px-1.5 py-0.5 rounded">
{transI18n("extra")}
</span>
<span className="text-sm font-medium text-primary/80">
{getLocaleName(locale, extra.Name)}
</span>
</div>
<span
className={`transition-transform ${
isOpen ? "rotate-90" : ""
}`}
>
▶
</span>
</div>
<div
className={`overflow-hidden transition-all duration-300 ${
isOpen ? "max-h-40 px-3 pb-2" : "max-h-0"
}`}
>
<div
className="text-sm opacity-90"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, extra.Desc),
extra.Param
)
}}
/>
</div>
</div>
)
})}
</div>
</div>
</div>
)
}
File diff suppressed because it is too large Load Diff
+18 -9
View File
@@ -1,9 +1,18 @@
export default function Footer() {
return (
<footer className="footer footer-horizontal footer-center bg-base-200 text-base-content rounded p-10">
<aside>
<p>Copyright © {new Date().getFullYear()} - Firefly Shelter (Powered by Nextjs & DaisyUi)</p>
</aside>
</footer>
)
}
export default function Footer() {
return (
<footer className="w-full border-t border-base-content/10 bg-base-100 py-6 px-4 mt-auto">
<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">
<div className="flex items-center gap-2">
<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>
)
}
File diff suppressed because it is too large Load Diff
+324 -267
View File
@@ -1,74 +1,107 @@
"use client"
import useUserDataStore from "@/stores/userDataStore";
import { useState, useMemo } from "react";
import useCopyProfileStore from "@/stores/copyProfile";
import ProfileCard from "../card/profileCard";
import { AvatarProfileCardType, AvatarProfileStore } from "@/types";
import Image from "next/image";
import { getNameChar, calcRarity } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import { useTranslations } from "next-intl";
import SelectCustomImage from "../select/customSelectImage";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
export default function CopyImport() {
const { avatars, setAvatar } = useUserDataStore();
const { avatarSelected } = useCurrentDataStore()
const { mapAvatar, baseType, damageType } = useDetailDataStore()
const { locale } = useLocaleStore()
const {
selectedProfiles,
avatarCopySelected,
setSelectedProfiles,
setAvatarCopySelected,
listElement,
listPath,
listRank,
setListElement,
setListPath,
setListRank
} = useCopyProfileStore()
const transI18n = useTranslations("DataPage")
const [message, setMessage] = useState({
type: "",
text: ""
})
const listAvatar = useMemo(() => {
if (!mapAvatar || !locale || !transI18n) return []
let list = Object.values(mapAvatar);
const allElementFalse = !Object.values(listElement).some(v => v)
const allPathFalse = !Object.values(listPath).some(v => v)
const allRarityFalse = !Object.values(listRank).some(v => v)
list = list.filter(item => (allElementFalse || listElement[item.DamageType]) && (allPathFalse || listPath[item.BaseType]) && (allRarityFalse || listRank[calcRarity(item.Rarity)]))
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return a.ID - b.ID
})
return list
}, [mapAvatar, listElement, listPath, listRank, locale, transI18n])
const handleProfileToggle = (profile: AvatarProfileCardType) => {
if (selectedProfiles.some((selectedProfile) => selectedProfile.key === profile.key)) {
setSelectedProfiles(selectedProfiles.filter((selectedProfile) => selectedProfile.key !== profile.key));
return;
}
setSelectedProfiles([...selectedProfiles, profile]);
};
const clearSelection = () => {
setSelectedProfiles([]);
setMessage({
type: "success",
text: transI18n("clearAll")
})
};
"use client"
import useUserDataStore from "@/stores/userDataStore";
import { useState, useMemo } from "react";
import useCopyProfileStore from "@/stores/copyProfile";
import ProfileCard from "../card/profileCard";
import { AvatarProfileCardType, AvatarProfileStore } from "@/types";
import Image from "next/image";
import { getNameChar, calcRarity } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import { useTranslations } from "next-intl";
import SelectCustomImage from "../select/customSelectImage";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
export default function CopyImport() {
const { avatars, setAvatar } = useUserDataStore(
useShallow((state) => ({
avatars: state.avatars,
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 {
selectedProfiles,
avatarCopySelected,
setSelectedProfiles,
setAvatarCopySelected,
listElement,
listPath,
listRank,
setListElement,
setListPath,
setListRank
} = 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 [message, setMessage] = useState({
type: "",
text: ""
})
const listAvatar = useMemo(() => {
if (!mapAvatar || !locale || !transI18n) return []
let list = Object.values(mapAvatar);
const allElementFalse = !Object.values(listElement).some(v => v)
const allPathFalse = !Object.values(listPath).some(v => v)
const allRarityFalse = !Object.values(listRank).some(v => v)
list = list.filter(item => (allElementFalse || listElement[item.DamageType]) && (allPathFalse || listPath[item.BaseType]) && (allRarityFalse || listRank[calcRarity(item.Rarity)]))
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return a.ID - b.ID
})
return list
}, [mapAvatar, listElement, listPath, listRank, locale, transI18n])
const handleProfileToggle = (profile: AvatarProfileCardType) => {
if (selectedProfiles.some((selectedProfile) => selectedProfile.key === profile.key)) {
setSelectedProfiles(selectedProfiles.filter((selectedProfile) => selectedProfile.key !== profile.key));
return;
}
setSelectedProfiles([...selectedProfiles, profile]);
};
const clearSelection = () => {
setSelectedProfiles([]);
setMessage({
type: "success",
text: transI18n("clearAll")
})
};
const selectAll = () => {
if (avatarCopySelected) {
const sourceAvatar = avatars[avatarCopySelected.ID.toString()]
@@ -79,32 +112,32 @@ export default function CopyImport() {
return null;
}
return {
key: index,
...profile,
} as AvatarProfileCardType
}).filter((profile) => profile !== null));
}
setMessage({
type: "success",
text: transI18n("selectAll")
})
};
const handleCopy = () => {
if (selectedProfiles.length === 0) {
setMessage({
type: "error",
text: transI18n("pleaseSelectAtLeastOneProfile")
});
return;
}
if (!avatarCopySelected) {
setMessage({
type: "error",
text: transI18n("noAvatarToCopySelected")
});
return;
}
key: index,
...profile,
} as AvatarProfileCardType
}).filter((profile) => profile !== null));
}
setMessage({
type: "success",
text: transI18n("selectAll")
})
};
const handleCopy = () => {
if (selectedProfiles.length === 0) {
setMessage({
type: "error",
text: transI18n("pleaseSelectAtLeastOneProfile")
});
return;
}
if (!avatarCopySelected) {
setMessage({
type: "error",
text: transI18n("noAvatarToCopySelected")
});
return;
}
if (!avatarSelected) {
setMessage({
type: "error",
@@ -129,181 +162,205 @@ export default function CopyImport() {
if (!selectedKeys.has(index) || (!profile.lightcone?.item_id && Object.keys(profile.relics ?? {}).length == 0)) {
return null;
}
return {
...profile,
profile_name: profile.profile_name + ` - Copy: ${avatarCopySelected?.ID}`,
} as AvatarProfileStore
}).filter((profile) => profile !== null);
const cloned = structuredClone(profile);
cloned.profile_name = profile.profile_name + ` - Copy: ${avatarCopySelected?.ID}`;
return cloned as AvatarProfileStore;
}).filter((profile): profile is AvatarProfileStore => profile !== null);
const newAvatar = {
...targetAvatar,
profileList: targetAvatar.profileList.concat(newListProfile),
profileList: [...targetAvatar.profileList, ...newListProfile],
profileSelect: targetAvatar.profileList.length + newListProfile.length - 1,
}
setAvatar(newAvatar);
setSelectedProfiles([]);
setMessage({
type: "success",
text: transI18n("copied")
});
};
return (
<div className="rounded-lg px-2 min-h-[60vh]">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-2">
<div className="mt-2 w-full">
<div className="flex items-start flex-col gap-2 mt-2 w-full">
<div>{transI18n("filter")}</div>
<div className="flex flex-wrap justify-start gap-5 md:gap-10 mt-2 w-full">
{/* Path */}
<div>
<div className="flex flex-wrap gap-2 justify-start items-center">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setListPath({ ...listPath, [key]: !listPath[key] })
}}
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
style={{
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
}}>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className="h-8 w-8 object-contain rounded-md"
width={200}
height={200}
/>
</div>
))}
</div>
</div>
{/* Element */}
<div>
<div className="flex flex-wrap gap-2 justify-start items-center">
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
setListElement({ ...listElement, [key]: !listElement[key] })
}}
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
style={{
backgroundColor: listElement[key] ? "#374151" : "#6B7280"
}}>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
width={200}
height={200}
/>
</div>
))}
</div>
</div>
{/* Rank */}
<div>
<div className="flex flex-wrap gap-2 justify-end items-center">
{Object.entries(listRank).map(([key], index) => (
<div
key={index}
onClick={() => {
setListRank({ ...listRank, [key]: !listRank[key] })
}}
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
style={{
backgroundColor: listRank[key] ? "#374151" : "#6B7280"
}}>
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">{key}*</div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-2">
{listAvatar.length > 0 && (
<div>
<div>{transI18n("characterName")}</div>
<SelectCustomImage
customSet={listAvatar.map((avatar) => ({
value: avatar.ID.toString(),
label: getNameChar(locale, transI18n, avatar),
imageUrl: `${process.env.CDN_URL}/${avatar.Image.AvatarIconPath}`
}))}
excludeSet={[]}
selectedCustomSet={avatarCopySelected?.ID.toString() || ""}
placeholder="Character Select"
setSelectedCustomSet={(value) => setAvatarCopySelected(mapAvatar[value] || null)}
/>
</div>
)}
</div>
</div>
{/* Selection Info */}
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
<span className="text-blue-400 font-medium">
{transI18n("selectedProfiles")}: {selectedProfiles.length}
</span>
<button
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"
>
{transI18n("clearAll")}
</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">
{transI18n("selectAll")}
</button>
{selectedProfiles.length > 0 && (
<button
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">
{transI18n("copy")}
</button>
)}
</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>
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
setAvatar(newAvatar);
setSelectedProfiles([]);
setMessage({
type: "success",
text: transI18n("copied")
});
};
return (
<div className="rounded-2xl p-2 sm:p-4 min-h-[50vh] space-y-4">
<div className="max-w-7xl mx-auto space-y-4">
{/* Header & Filters Card */}
<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="flex flex-wrap items-center gap-3 sm:gap-4">
{/* Path Filters */}
<div className="flex flex-wrap items-center gap-1.5 sm:gap-2">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => {
const isActive = !!listPath[key];
return (
<button
key={key}
type="button"
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 ${
isActive
? "bg-primary/20 border-primary ring-2 ring-primary/50 shadow-sm scale-105"
: "bg-base-200 hover:bg-base-300 border-base-300 opacity-80 hover:opacity-100"
}`}
title={key}
>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className={`w-6 h-6 sm:w-7 sm:h-7 object-contain ${
isActive ? "theme-path-icon-active" : "theme-path-icon"
}`}
width={32}
height={32}
/>
</button>
);
})}
</div>
<div className="h-8 w-px bg-base-300 hidden md:block" />
{/* Element Filters */}
<div className="flex flex-wrap items-center gap-1.5 sm:gap-2">
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => {
const isActive = !!listElement[key];
return (
<button
key={key}
type="button"
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 ${
isActive
? "bg-primary/20 border-primary ring-2 ring-primary/50 shadow-sm scale-105"
: "bg-base-200 hover:bg-base-300 border-base-300 opacity-80 hover:opacity-100"
}`}
title={key}
>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className="w-6 h-6 sm:w-7 sm:h-7 object-contain drop-shadow-xs"
width={32}
height={32}
/>
</button>
);
})}
</div>
<div className="h-8 w-px bg-base-300 hidden md:block" />
{/* Rank Filters */}
<div className="flex items-center gap-1.5 sm:gap-2">
{Object.entries(listRank).map(([key], index) => {
const isActive = !!listRank[key];
return (
<button
key={index}
type="button"
onClick={() => setListRank({ ...listRank, [key]: !listRank[key] })}
className={`h-10 px-3.5 sm:h-12 sm:px-4.5 rounded-2xl transition-all duration-150 active-press border flex items-center justify-center text-xs sm:text-sm font-mono font-bold cursor-pointer ${
isActive
? "bg-primary text-primary-content border-primary shadow-sm scale-105"
: "bg-base-200 hover:bg-base-300 text-base-content/70 border-base-300"
}`}
>
{key}★
</button>
);
})}
</div>
</div>
</div>
{/* Character Select Dropdown */}
{listAvatar.length > 0 && (
<div className="space-y-1.5 pt-2 border-t border-base-300/60">
<div className="text-xs font-bold text-base-content/70">{transI18n("characterName")}</div>
<SelectCustomImage
customSet={listAvatar.map((avatar) => ({
value: avatar.ID.toString(),
label: getNameChar(locale, transI18n, avatar),
imageUrl: `${process.env.CDN_URL}/${avatar.Image.AvatarIconPath}`
}))}
excludeSet={[]}
selectedCustomSet={avatarCopySelected?.ID.toString() || ""}
placeholder="Character Select"
setSelectedCustomSet={(value) => setAvatarCopySelected(mapAvatar[value] || null)}
/>
</div>
)}
</div>
{/* Selection Action Bar */}
<div className="bg-base-200 border border-base-300 rounded-2xl p-3.5 flex flex-wrap items-center justify-between gap-3 shadow-xs">
<div className="flex items-center gap-2">
<span className="text-xs sm:text-sm font-bold text-base-content">
{transI18n("selectedProfiles")}:
</span>
<span className="badge badge-primary font-mono font-bold text-xs">
{selectedProfiles.length}
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={clearSelection}
className="btn btn-xs sm:btn-sm btn-ghost text-error hover:bg-error/10 border border-error/30 rounded-xl"
>
{transI18n("clearAll")}
</button>
<button
onClick={selectAll}
className="btn btn-xs sm:btn-sm btn-ghost text-primary hover:bg-primary/10 border border-primary/30 rounded-xl"
>
{transI18n("selectAll")}
</button>
{selectedProfiles.length > 0 && (
<button
onClick={handleCopy}
className="btn btn-xs sm:btn-sm btn-primary rounded-xl font-bold shadow-xs"
>
{transI18n("copy")}
</button>
)}
</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 */}
<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) => {
if (!profile.lightcone?.item_id && Object.keys(profile.relics ?? {}).length == 0) {
return null;
}
return (
<ProfileCard
key={index}
profile={{ ...profile, key: index }}
selectedProfile={selectedProfiles}
onProfileToggle={handleProfileToggle}
/>
)
})}
</div>
</div>
</div>
)
return null;
}
return (
<ProfileCard
key={index}
profile={{ ...profile, key: index }}
selectedProfile={selectedProfiles}
onProfileToggle={handleProfileToggle}
/>
)
})}
</div>
</div>
</div>
)
}
+173 -136
View File
@@ -1,10 +1,10 @@
"use client"
import { useState } from "react";
import CharacterInfoCard from "../card/characterInfoCard";
import useEnkaStore from "@/stores/enkaStore";
import { SendDataThroughProxy } from "@/lib/api/api";
import { CharacterInfoCardType, EnkaResponse } from "@/types";
import useUserDataStore from "@/stores/userDataStore";
"use client"
import { useState } from "react";
import CharacterInfoCard from "../card/characterInfoCard";
import useEnkaStore from "@/stores/enkaStore";
import { SendDataThroughProxy } from "@/lib/api/api";
import { CharacterInfoCardType, EnkaResponse } from "@/types";
import useUserDataStore from "@/stores/userDataStore";
import { converterOneEnkaDataToAvatarStore } from "@/helper";
import useModelStore from "@/stores/modelStore";
import { toast } from "react-toastify";
@@ -12,6 +12,7 @@ import { useTranslations } from "next-intl";
import { avatarEnkaDetailSchema, enkaResponseSchema } from "@/zod";
import { z } from "zod";
import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
type AvatarEnkaDetailInput = z.infer<typeof avatarEnkaDetailSchema>;
@@ -31,24 +32,46 @@ const toCharacterInfoCard = (character: AvatarEnkaDetailInput): CharacterInfoCar
relic_set_id: parseInt(relic.tid.toString().slice(1, -1), 10),
})),
});
export default function EnkaImport() {
const {
uidInput,
setUidInput,
enkaData,
selectedCharacters,
setSelectedCharacters,
setEnkaData,
} = useEnkaStore();
const transI18n = useTranslations("DataPage")
const { avatars, setAvatar } = useUserDataStore();
const { setIsOpenImport } = useModelStore()
const { mapAvatar } = useDetailDataStore()
export default function EnkaImport() {
const {
uidInput,
setUidInput,
enkaData,
selectedCharacters,
setSelectedCharacters,
setEnkaData,
} = 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 { avatars, setAvatar } = useUserDataStore(
useShallow((state) => ({
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 [Error, setError] = useState("")
const validCharacters = enkaData?.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId]) ?? []
const handlerFetchData = async () => {
if (!uidInput) {
setError(transI18n("pleaseEnterUid"))
@@ -77,36 +100,36 @@ export default function EnkaImport() {
setIsLoading(false)
}
}
const handleCharacterToggle = (character: CharacterInfoCardType) => {
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
return;
}
setSelectedCharacters([...selectedCharacters, character]);
};
const clearSelection = () => {
setSelectedCharacters([]);
};
const handleCharacterToggle = (character: CharacterInfoCardType) => {
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
return;
}
setSelectedCharacters([...selectedCharacters, character]);
};
const clearSelection = () => {
setSelectedCharacters([]);
};
const selectAll = () => {
if (enkaData) {
setSelectedCharacters(validCharacters.map(toCharacterInfoCard));
}
};
const handleImport = () => {
if (selectedCharacters.length === 0) {
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
return;
}
if (!enkaData) {
setError(transI18n("noDataToImport"));
return;
}
setError("");
const listAvatars = { ...avatars }
const handleImport = () => {
if (selectedCharacters.length === 0) {
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
return;
}
if (!enkaData) {
setError(transI18n("noDataToImport"));
return;
}
setError("");
const listAvatars = { ...avatars }
const parsed = enkaResponseSchema.safeParse(enkaData)
if (!parsed.success) {
setError(transI18n("failedToFetchEnkaData"));
@@ -115,90 +138,104 @@ export default function EnkaImport() {
const filterData = parsed.data.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId] && selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatarId))
filterData.forEach((character) => {
const newAvatar = { ...listAvatars[character.avatarId.toString()] }
if (Object.keys(newAvatar).length !== 0) {
newAvatar.level = character.level ?? 0
newAvatar.promotion = character.promotion ?? 0
newAvatar.data = {
rank: character.rank ?? 0,
skills: character.skillTreeList.reduce((acc, skill) => {
acc[skill.pointId] = skill.level;
return acc;
}, {} as Record<string, number>),
}
const newProfile = converterOneEnkaDataToAvatarStore(character, newAvatar.profileList.length)
if (newProfile) {
newAvatar.profileList.push(newProfile)
newAvatar.profileSelect = newAvatar.profileList.length - 1
}
setAvatar(newAvatar)
}
})
setIsOpenImport(false)
toast.success(transI18n("importEnkaDataSuccess"))
};
return (
<div className="bg-slate-900 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-white text-2xl font-bold mb-6">HSR UID</h1>
<div className="flex gap-4 items-center mb-6">
<input
type="text"
value={uidInput}
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"
placeholder="Enter UID"
/>
<button onClick={handlerFetchData} className="btn btn-success rounded-lg transition-colors">
{transI18n("getData")}
</button>
{selectedCharacters.length > 0 && (
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
{transI18n("import")}
</button>
)}
</div>
{Error && (
<div className="text-error text-base mt-2">😭 {Error}</div>
)}
{/* Player Info */}
{enkaData && (
<div className="text-white space-y-2">
<div className="text-lg">Name: {enkaData.detailInfo.nickname}</div>
<div className="text-lg">UID: {enkaData.detailInfo.uid}</div>
<div className="text-lg">Level: {enkaData.detailInfo.level}</div>
</div>
)}
{/* Selection Info */}
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
<span className="text-blue-400 font-medium">
{transI18n("selectedCharacters")}: {selectedCharacters.length}
</span>
<button
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"
>
{transI18n("clearAll")}
</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">
{transI18n("selectAll")}
</button>
</div>
</div>
{isLoading && (
<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>
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
const currentAvatar = listAvatars[character.avatarId.toString()]
if (currentAvatar && Object.keys(currentAvatar).length !== 0) {
const newProfileList = [...(currentAvatar.profileList || [])]
const newProfile = converterOneEnkaDataToAvatarStore(character, newProfileList.length)
if (newProfile) {
newProfileList.push(newProfile)
}
const newAvatar = {
...currentAvatar,
level: character.level ?? 0,
promotion: character.promotion ?? 0,
data: {
rank: character.rank ?? 0,
skills: character.skillTreeList.reduce((acc, skill) => {
acc[skill.pointId] = skill.level;
return acc;
}, {} as Record<string, number>),
},
profileList: newProfileList,
profileSelect: newProfile ? newProfileList.length - 1 : currentAvatar.profileSelect,
}
setAvatar(newAvatar)
}
})
setIsOpenImport(false)
toast.success(transI18n("importEnkaDataSuccess"))
};
return (
<div className="bg-base-100 p-4 sm:p-6 rounded-2xl">
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="space-y-4">
<h1 className="text-base-content text-xl sm:text-2xl font-bold">HSR UID</h1>
<div className="flex flex-wrap gap-3 items-center">
<input
type="text"
value={uidInput}
onChange={(e) => setUidInput(e.target.value)}
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"
/>
<button onClick={handlerFetchData} className="btn btn-sm btn-success rounded-xl font-bold">
{transI18n("getData")}
</button>
{selectedCharacters.length > 0 && (
<button onClick={handleImport} className="btn btn-sm btn-primary rounded-xl font-bold shadow-xs">
{transI18n("import")}
</button>
)}
</div>
{Error && (
<div className="text-error text-sm font-semibold mt-2">😭 {Error}</div>
)}
{/* Player Info */}
{enkaData && (
<div className="bg-base-200 p-4 rounded-xl border border-base-300 text-base-content space-y-1">
<div className="text-sm font-semibold">Name: <span className="font-bold text-primary">{enkaData.detailInfo.nickname}</span></div>
<div className="text-sm font-semibold">UID: <span className="font-mono">{enkaData.detailInfo.uid}</span></div>
<div className="text-sm font-semibold">Level: <span className="font-bold">{enkaData.detailInfo.level}</span></div>
</div>
)}
{/* Selection Info */}
<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">
<div className="flex items-center gap-2">
<span className="text-xs sm:text-sm font-bold text-base-content">
{transI18n("selectedCharacters")}:
</span>
<span className="badge badge-primary font-mono font-bold text-xs">
{selectedCharacters.length}
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={clearSelection}
className="btn btn-xs sm:btn-sm btn-ghost text-error hover:bg-error/10 border border-error/30 rounded-xl"
>
{transI18n("clearAll")}
</button>
<button
onClick={selectAll}
className="btn btn-xs sm:btn-sm btn-ghost text-primary hover:bg-primary/10 border border-primary/30 rounded-xl"
>
{transI18n("selectAll")}
</button>
</div>
</div>
</div>
{isLoading && (
<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>
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{validCharacters.map((character) => (
<CharacterInfoCard
key={character.avatarId}
@@ -206,9 +243,9 @@ export default function EnkaImport() {
selectedCharacters={selectedCharacters}
onCharacterToggle={handleCharacterToggle}
/>
))}
</div>
</div>
</div>
);
))}
</div>
</div>
</div>
);
}
+172 -138
View File
@@ -1,16 +1,17 @@
"use client"
import useFreeSRStore from "@/stores/freesrStore";
import useModelStore from "@/stores/modelStore";
import useUserDataStore from "@/stores/userDataStore";
import { CharacterInfoCardType } from "@/types";
import { useState } from "react";
import CharacterInfoCard from "../card/characterInfoCard";
import { freeSrJsonSchema } from "@/zod";
import { toast } from "react-toastify";
import { converterOneFreeSRDataToAvatarStore } from "@/helper";
import { useTranslations } from "next-intl";
"use client"
import useFreeSRStore from "@/stores/freesrStore";
import useModelStore from "@/stores/modelStore";
import useUserDataStore from "@/stores/userDataStore";
import { CharacterInfoCardType } from "@/types";
import { useState } from "react";
import CharacterInfoCard from "../card/characterInfoCard";
import { freeSrJsonSchema } from "@/zod";
import { toast } from "react-toastify";
import { converterOneFreeSRDataToAvatarStore } from "@/helper";
import { useTranslations } from "next-intl";
import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
import { z } from "zod";
type FreeSRJsonInput = z.infer<typeof freeSrJsonSchema>;
@@ -36,55 +37,75 @@ const toCharacterInfoCard = (data: FreeSRJsonInput, character: FreeSRJsonInput["
})),
}
}
export default function FreeSRImport() {
const { avatars, setAvatar } = useUserDataStore();
const { mapAvatar } = useDetailDataStore();
const { setIsOpenImport } = useModelStore()
const [isLoading, setIsLoading] = useState(false)
export default function FreeSRImport() {
const { avatars, setAvatar } = useUserDataStore(
useShallow((state) => ({
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 [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 parsedFreeSRData = freeSrJsonSchema.safeParse(freeSRData)
const validFreeSRData = parsedFreeSRData.success ? parsedFreeSRData.data : null
const handleCharacterToggle = (character: CharacterInfoCardType) => {
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
return;
}
setSelectedCharacters([...selectedCharacters, character]);
};
const clearSelection = () => {
setSelectedCharacters([]);
};
const handleCharacterToggle = (character: CharacterInfoCardType) => {
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
return;
}
setSelectedCharacters([...selectedCharacters, character]);
};
const clearSelection = () => {
setSelectedCharacters([]);
};
const selectAll = () => {
const parsed = freeSrJsonSchema.safeParse(freeSRData)
if (parsed.success) {
setSelectedCharacters(Object.values(parsed.data.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => toCharacterInfoCard(parsed.data, character)));
}
};
const handlerReadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsLoading(true)
const file = event.target.files?.[0];
if (!file) {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("pleaseSelectAFile"))
setIsLoading(false)
return
}
if (!file.name.endsWith(".json") || file.type !== "application/json") {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("fileMustBeAValidJsonFile"))
setIsLoading(false)
return
}
const handlerReadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
setIsLoading(true)
const file = event.target.files?.[0];
if (!file) {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("pleaseSelectAFile"))
setIsLoading(false)
return
}
if (!file.name.endsWith(".json") || file.type !== "application/json") {
setSelectedCharacters([])
setFreeSRData(null)
setError(transI18n("fileMustBeAValidJsonFile"))
setIsLoading(false)
return
}
if (file) {
const reader = new FileReader();
@@ -121,18 +142,18 @@ export default function FreeSRImport() {
reader.readAsText(file);
}
};
const handleImport = () => {
if (selectedCharacters.length === 0) {
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
return;
}
if (!freeSRData) {
setError(transI18n("noDataToImport"));
return;
}
setError("");
const handleImport = () => {
if (selectedCharacters.length === 0) {
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
return;
}
if (!freeSRData) {
setError(transI18n("noDataToImport"));
return;
}
setError("");
const parsed = freeSrJsonSchema.safeParse(freeSRData)
if (!parsed.success) {
setError(transI18n("fileMustBeAValidJsonFile"));
@@ -142,75 +163,88 @@ export default function FreeSRImport() {
const listAvatars = { ...avatars }
const filterData = Object.values(parsed.data.avatars).filter((character) => selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id))
filterData.forEach((character) => {
const newAvatar = { ...listAvatars[character.avatar_id] }
if (Object.keys(newAvatar).length !== 0) {
newAvatar.level = character.level
newAvatar.promotion = character.promotion
newAvatar.data = {
rank: character.data.rank ?? 0,
skills: character.data.skills
}
const newProfile = converterOneFreeSRDataToAvatarStore(parsed.data, newAvatar.profileList.length, character.avatar_id)
if (newProfile) {
newAvatar.profileList.push(newProfile)
newAvatar.profileSelect = newAvatar.profileList.length - 1
}
setAvatar(newAvatar)
}
})
setIsOpenImport(false)
toast.success(transI18n("importFreeSRDataSuccess"))
}
return (
<div className="bg-slate-900 p-6">
<div className="max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<h1 className="text-white text-2xl font-bold mb-6">{transI18n("freeSRImport")}</h1>
<div className="flex gap-4 items-center mb-6">
<fieldset className="fieldset">
<legend className="fieldset-legend">{transI18n("pickAFile")}</legend>
<input type="file" accept=".json" className="file-input file-input-info" onChange={handlerReadFile} />
<label className="label">{transI18n("onlySupportFreeSRJsonFile")}</label>
</fieldset>
{selectedCharacters.length > 0 && (
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
{transI18n("import")}
</button>
)}
</div>
{Error && (
<div className="text-error text-base mt-2">😭 {Error}</div>
)}
{/* Selection Info */}
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
<span className="text-blue-400 font-medium">
{transI18n("selectedCharacters")}: {selectedCharacters.length}
</span>
<button
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"
>
{transI18n("clearAll")}
</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">
{transI18n("selectAll")}
</button>
</div>
</div>
{isLoading && (
<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>
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
const currentAvatar = listAvatars[character.avatar_id]
if (currentAvatar && Object.keys(currentAvatar).length !== 0) {
const newProfileList = [...(currentAvatar.profileList || [])]
const newProfile = converterOneFreeSRDataToAvatarStore(parsed.data, newProfileList.length, character.avatar_id)
if (newProfile) {
newProfileList.push(newProfile)
}
const newAvatar = {
...currentAvatar,
level: character.level,
promotion: character.promotion,
data: {
rank: character.data.rank ?? 0,
skills: character.data.skills
},
profileList: newProfileList,
profileSelect: newProfile ? newProfileList.length - 1 : currentAvatar.profileSelect,
}
setAvatar(newAvatar)
}
})
setIsOpenImport(false)
toast.success(transI18n("importFreeSRDataSuccess"))
}
return (
<div className="bg-base-100 p-4 sm:p-6 rounded-2xl">
<div className="max-w-7xl mx-auto space-y-6">
{/* Header */}
<div className="space-y-4">
<h1 className="text-base-content text-xl sm:text-2xl font-bold">{transI18n("freeSRImport")}</h1>
<div className="flex flex-wrap gap-4 items-center">
<fieldset className="fieldset">
<legend className="fieldset-legend text-xs font-semibold">{transI18n("pickAFile")}</legend>
<input type="file" accept=".json" className="file-input file-input-sm file-input-bordered rounded-xl" onChange={handlerReadFile} />
<label className="label text-[11px] text-base-content/60">{transI18n("onlySupportFreeSRJsonFile")}</label>
</fieldset>
{selectedCharacters.length > 0 && (
<button onClick={handleImport} className="btn btn-sm btn-primary rounded-xl font-bold shadow-xs">
{transI18n("import")}
</button>
)}
</div>
{Error && (
<div className="text-error text-sm font-semibold mt-2">😭 {Error}</div>
)}
{/* Selection Info */}
<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">
<div className="flex items-center gap-2">
<span className="text-xs sm:text-sm font-bold text-base-content">
{transI18n("selectedCharacters")}:
</span>
<span className="badge badge-primary font-mono font-bold text-xs">
{selectedCharacters.length}
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={clearSelection}
className="btn btn-xs sm:btn-sm btn-ghost text-error hover:bg-error/10 border border-error/30 rounded-xl"
>
{transI18n("clearAll")}
</button>
<button
onClick={selectAll}
className="btn btn-xs sm:btn-sm btn-ghost text-primary hover:bg-primary/10 border border-primary/30 rounded-xl"
>
{transI18n("selectAll")}
</button>
</div>
</div>
</div>
{isLoading && (
<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>
)}
{/* Character Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
{validFreeSRData && Object.values(validFreeSRData.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => {
return (
<CharacterInfoCard
@@ -219,10 +253,10 @@ export default function FreeSRImport() {
selectedCharacters={selectedCharacters}
onCharacterToggle={handleCharacterToggle}
/>
)
})}
</div>
</div>
</div>
)
)
})}
</div>
</div>
</div>
)
}
+211 -144
View File
@@ -1,145 +1,212 @@
"use client"
import { useMemo } from "react"
import Image from "next/image";
import useLocaleStore from "@/stores/localeStore"
import LightconeCard from "../card/lightconeCard";
import useUserDataStore from "@/stores/userDataStore";
import useModelStore from "@/stores/modelStore";
import { useTranslations } from "next-intl";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { calcRarity, getLocaleName } from "@/helper";
export default function LightconeBar() {
const { locale } = useLocaleStore()
const {
avatarSelected,
mapLightconePathActive,
mapLightconeRankActive,
setMapLightconePathActive,
setMapLightconeRankActive,
lightconeSearch,
setLightconeSearch
} = useCurrentDataStore()
const { setAvatar, avatars } = useUserDataStore()
const { setIsOpenLightcone } = useModelStore()
const { mapLightCone, baseType } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const listLightcone = useMemo(() => {
if (!mapLightCone || !locale) return []
let list = Object.values(mapLightCone)
if (lightconeSearch) {
list = list.filter(item => getLocaleName(locale, item.Name).toLowerCase().includes(lightconeSearch.toLowerCase()))
}
const allRankFalse = !Object.values(mapLightconeRankActive).some(v => v)
const allPathFalse = !Object.values(mapLightconePathActive).some(v => v)
list = list.filter(item =>
(allRankFalse || mapLightconeRankActive[item.Rarity]) &&
(allPathFalse || mapLightconePathActive[item.BaseType])
)
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return b.ID - a.ID
})
return list
}, [mapLightCone, mapLightconePathActive, mapLightconeRankActive, lightconeSearch, locale])
return (
<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">
{transI18n("lightConeSetting")}
</h3>
</div>
<div className="mt-2 w-full">
<div className="flex items-start flex-col gap-2">
<div>Search</div>
<input
value={lightconeSearch}
onChange={(e) => setLightconeSearch(e.target.value)}
type="text" placeholder="LightCone Name" className="input input-accent mt-1 w-full"
/>
</div>
<div className="flex items-start flex-col gap-2 mt-2 w-full">
<div>Filter</div>
<div className="flex flex-row flex-wrap justify-between mt-1 w-full">
<div className="flex flex-wrap mb-1 mx-1 gap-2">
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
<div
key={key}
onClick={() => {
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"
style={{
backgroundColor: mapLightconePathActive[key] ? "#374151" : "#6B7280"
}}
>
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className="h-7 w-7 md:h-8 md:w-8 object-contain rounded-md"
width={200}
height={200}
/>
</div>
))}
</div>
<div className="flex flex-wrap mb-1 mx-1 gap-2">
{Object.keys(mapLightconeRankActive).map((key, index) => (
<div
key={index}
onClick={() => {
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"
style={{
backgroundColor: mapLightconeRankActive[key] ? "#374151" : "#6B7280"
}}
>
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">
{key}*
</div>
</div>
))}
</div>
</div>
</div>
</div>
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-8 mt-2 gap-2">
{listLightcone.map((item, index) => (
<div key={index} onClick={() => {
if (avatarSelected) {
const avatar = avatars[avatarSelected?.ID?.toString()]
avatar.profileList[avatar.profileSelect].lightcone = {
level: 80,
item_id: item.ID,
rank: 1,
promotion: 6
}
setAvatar({ ...avatar })
setIsOpenLightcone(false)
}
}}>
<LightconeCard data={item} />
</div>
))}
</div>
</div>
)
"use client"
import { useMemo } from "react"
import Image from "next/image";
import useLocaleStore from "@/stores/localeStore"
import LightconeCard from "../card/lightconeCard";
import useUserDataStore from "@/stores/userDataStore";
import useModelStore from "@/stores/modelStore";
import { useTranslations } from "next-intl";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { calcRarity, getLocaleName } from "@/helper";
import { useShallow } from "zustand/react/shallow";
import { Search, X } from "lucide-react";
export default function LightconeBar() {
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const {
avatarSelected,
mapLightconePathActive,
mapLightconeRankActive,
setMapLightconePathActive,
setMapLightconeRankActive,
lightconeSearch,
setLightconeSearch
} = useCurrentDataStore(
useShallow((state) => ({
avatarSelected: state.avatarSelected,
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 listLightcone = useMemo(() => {
if (!mapLightCone || !locale) return []
let list = Object.values(mapLightCone)
if (lightconeSearch) {
list = list.filter(item => getLocaleName(locale, item.Name).toLowerCase().includes(lightconeSearch.toLowerCase()))
}
const allRankFalse = !Object.values(mapLightconeRankActive).some(v => v)
const allPathFalse = !Object.values(mapLightconePathActive).some(v => v)
list = list.filter(item => {
const itemRank = calcRarity(item.Rarity).toString();
return (
(allRankFalse || !!mapLightconeRankActive[itemRank] || !!mapLightconeRankActive[item.Rarity]) &&
(allPathFalse || !!mapLightconePathActive[item.BaseType])
);
})
list.sort((a, b) => {
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
if (r !== 0) return r
return b.ID - a.ID
})
return list
}, [mapLightCone, mapLightconePathActive, mapLightconeRankActive, lightconeSearch, locale])
return (
<div className="space-y-4">
{/* Search & Filters */}
<div className="p-3 rounded-2xl bg-base-200 border border-base-300 space-y-2.5">
{/* Search Bar */}
<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">
<Search className="w-4 h-4 text-base-content/60 shrink-0" />
<input
value={lightconeSearch}
onChange={(e) => setLightconeSearch(e.target.value)}
type="text"
placeholder={`${transI18n("lightcones")}...`}
className="grow text-xs bg-transparent border-none outline-none focus:outline-none"
/>
{lightconeSearch && (
<button
type="button"
onClick={() => setLightconeSearch("")}
className="text-base-content/40 hover:text-base-content"
>
<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}
type="button"
onClick={() => {
setMapLightconePathActive({ ...mapLightconePathActive, [key]: !mapLightconePathActive[key] })
}}
className={`h-7 w-7 rounded-lg transition-all duration-150 active-press border flex items-center justify-center ${
isActive
? "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
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${value.Icon}`}
alt={key}
className={`w-4.5 h-4.5 object-contain ${
isActive ? "theme-path-icon-active" : "theme-path-icon"
}`}
width={20}
height={20}
/>
</button>
);
})}
</div>
{/* Rarity Filter Chips */}
<div className="flex items-center gap-1.5">
{Object.keys(mapLightconeRankActive).map((key) => {
const isActive = !!mapLightconeRankActive[key];
return (
<button
key={key}
type="button"
onClick={() => {
setMapLightconeRankActive({ ...mapLightconeRankActive, [key]: !mapLightconeRankActive[key] })
}}
className={`px-3 py-1 rounded-xl text-xs font-mono font-bold transition-all duration-200 active-press border ${
isActive
? "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"
}`}
>
{key}★
</button>
);
})}
</div>
</div>
</div>
{/* Results Grid */}
<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">
{listLightcone.map((item) => (
<div
key={item.ID}
className="h-full flex flex-col"
onClick={() => {
if (avatarSelected) {
const avatar = avatars[avatarSelected.ID.toString()]
if (avatar) {
const profileIndex = avatar.profileSelect
const currentProfile = avatar.profileList?.[profileIndex]
if (currentProfile) {
const newProfileList = [...avatar.profileList]
newProfileList[profileIndex] = {
...currentProfile,
lightcone: {
level: 80,
item_id: item.ID,
rank: 1,
promotion: 6
}
}
setAvatar({
...avatar,
profileList: newProfileList
})
setIsOpenLightcone(false)
}
}
}
}}
>
<LightconeCard data={item} />
</div>
))}
</div>
</div>
)
}
+33 -10
View File
@@ -8,14 +8,30 @@ import Image from "next/image";
import { ASEvent, MonsterStore } from "@/types";
import { useTranslations } from "next-intl";
import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
export default function AsBar() {
const { locale } = useLocaleStore()
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const {
as_config,
setAsConfig
} = useUserDataStore()
const { mapMonster, mapAS, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
} = useUserDataStore(
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 challengeSelected = useMemo(() => {
@@ -108,6 +124,13 @@ export default function AsBar() {
if (targetEventList.length > 0) {
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) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
@@ -135,10 +158,10 @@ export default function AsBar() {
if (!mapAS) return null
return (
<div className="py-8 relative">
<div className="pt-4 pb-2 relative space-y-4">
{/* 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">
<SelectCustomText
customSet={Object.values(mapAS).sort((a, b) => b.ID - a.ID).map((as) => ({
@@ -209,13 +232,13 @@ export default function AsBar() {
</div>
)}
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2>
<div className="pt-3 border-t border-base-content/10 space-y-2">
<h2 className="text-xl font-bold text-info">{transI18n("turbulenceBuff")}</h2>
{challengeSelected ? (
challengeSelected.MazeBuff.map((buff, i) => (
<div
key={i}
className="text-base"
className="text-sm text-base-content/85 leading-relaxed"
dangerouslySetInnerHTML={{
__html: replaceByParam(
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>
@@ -246,7 +269,7 @@ export default function AsBar() {
const targetEvent = eventList[0];
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>
{targetEvent?.MonsterList?.map((wave, waveIndex) => (
+75 -69
View File
@@ -17,19 +17,37 @@ import { useTranslations } from "next-intl";
import useGlobalStore from "@/stores/globalStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { MonsterDetail } from "@/types";
import { useShallow } from "zustand/react/shallow";
export default function CeBar() {
const [searchTerm, setSearchTerm] = useState("");
const [showSearchWaveId, setShowSearchWaveId] = useState<number | null>(null);
const { ce_config, setCeConfig } = useUserDataStore()
const { mapMonster, stage, damageType } = useDetailDataStore()
const { locale } = useLocaleStore()
const { ce_config, setCeConfig } = useUserDataStore(
useShallow((state) => ({
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 [showSearchStage, setShowSearchStage] = useState(false)
const [stageSearchTerm, setStageSearchTerm] = useState("")
const [stagePage, setStagePage] = useState(1)
const { extraData, setExtraData } = useGlobalStore()
const { extraData, setExtraData } = useGlobalStore(
useShallow((state) => ({
extraData: state.extraData,
setExtraData: state.setExtraData,
}))
)
const pageSize = 30
@@ -59,10 +77,6 @@ export default function CeBar() {
[monsterPage, filteredMonsters]
)
useEffect(() => {
setMonsterPage(1)
}, [searchTerm])
const stageList = useMemo(() => Object.values(stage).map((item) => ({
id: item.ID.toString(),
name: `${getLocaleName(locale, item.Name)} (${item.ID})`,
@@ -79,11 +93,6 @@ export default function CeBar() {
const hasMorePages = useMemo(() => stagePage * pageSize < filteredStages.length, [stagePage, filteredStages])
useEffect(() => {
setStagePage(1)
}, [stageSearchTerm])
useEffect(() => {
if (!ce_config) return
if (!extraData || !extraData.theory_craft?.mode) return
@@ -110,27 +119,26 @@ export default function CeBar() {
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)
setShowSearchStage(false)
}}>
<div className="mb-4 w-full relative">
<div className="w-full relative">
<div className="flex items-center justify-center gap-2">
<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) => {
e.stopPropagation()
setShowSearchStage(!showSearchStage)
}}
>
<Search className="w-6 h-6" />
<span className="text-left"> {transI18n("stage")}: {stageList.find((s) => s.id === ce_config.stage_id.toString())?.name || transI18n("selectStage")}</span>
<Search className="w-4 h-4" />
<span className="text-left font-medium"> {transI18n("stage")}: {stageList.find((s) => s.id === ce_config.stage_id.toString())?.name || transI18n("selectStage")}</span>
</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 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">
<label className="input w-full">
@@ -147,46 +155,44 @@ export default function CeBar() {
</g>
</svg>
<input
type="search" className="grow"
placeholder={transI18n("searchStage")}
value={stageSearchTerm}
onChange={(e) => setStageSearchTerm(e.target.value)}
autoFocus
onChange={(e) => {
setStageSearchTerm(e.target.value)
setStagePage(1)
}}
type="search"
required
placeholder={transI18n("searchStage")}
/>
</label>
</div>
<div className="max-h-60 overflow-y-auto space-y-1">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 max-h-[48vh] overflow-y-auto pr-1">
{paginatedStages.length > 0 ? (
<>
{paginatedStages.map((stage) => (
<div
key={stage.id}
className="p-2 hover:bg-primary/20 rounded cursor-pointer"
onClick={() => {
if (ce_config.stage_id !== Number(stage.id)) {
setCeConfig({ ...ce_config, stage_id: Number(stage.id), cycle_count: 30 })
}
setShowSearchStage(false)
setStageSearchTerm("")
}}
>
{stage.name}
</div>
))}
</>
paginatedStages.map((stage) => (
<button
key={stage.id}
className="btn btn-sm btn-outline justify-start"
onClick={(e) => {
e.stopPropagation()
if (ce_config.stage_id !== Number(stage.id)) {
setCeConfig({ ...ce_config, stage_id: Number(stage.id), cycle_count: 30 })
}
setShowSearchStage(false)
setStageSearchTerm("")
}}
>
<span className="truncate">{stage.name}</span>
</button>
))
) : (
<div className="text-sm text-center py-4">{transI18n("noStageFound")}</div>
)}
</div>
{paginatedStages.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mt-4">
<button
disabled={stagePage === 1}
className="btn btn-sm btn-outline btn-success mt-2"
className="btn btn-sm btn-outline btn-success"
onClick={(e) => {
e.stopPropagation()
setStagePage(stagePage - 1)
@@ -197,7 +203,7 @@ export default function CeBar() {
<button
disabled={!hasMorePages}
className="btn btn-sm btn-outline btn-success mt-2"
className="btn btn-sm btn-outline btn-success"
onClick={(e) => {
e.stopPropagation()
setStagePage(stagePage + 1)
@@ -211,12 +217,12 @@ export default function CeBar() {
)}
</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) => (
<div key={waveIndex} className="card border border-slate-700/50 ">
<div className="card-body p-6">
<div key={waveIndex} className="rounded-xl border border-warning p-4 bg-base-100">
<div className="w-full">
<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">
<button
disabled={waveIndex === 0}
@@ -430,7 +436,10 @@ export default function CeBar() {
type="search" className="grow"
placeholder={transI18n("searchMonster")}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onChange={(e) => {
setSearchTerm(e.target.value)
setMonsterPage(1)
}}
autoFocus
/>
@@ -513,20 +522,17 @@ export default function CeBar() {
))}
{/* Add New Wave Button */}
<div className="card border border-slate-700/50 border-dashed">
<div className="card-body p-8">
<button
onClick={() => {
const newCeConfig = structuredClone(ce_config)
newCeConfig.monsters.push([])
setCeConfig(newCeConfig)
}}
className="btn btn-primary btn-lg w-full">
<Plus className="w-6 h-6 mr-2" />
{transI18n("addNewWave")}
</button>
</div>
</div>
<button
onClick={() => {
const newCeConfig = structuredClone(ce_config)
newCeConfig.monsters.push([])
setCeConfig(newCeConfig)
}}
className="btn btn-outline btn-warning w-full flex items-center justify-center gap-2 rounded-xl shadow-xs font-semibold"
>
<Plus className="w-4 h-4" />
<span>{transI18n("addNewWave")}</span>
</button>
</div>
</div>
);
+49 -33
View File
@@ -6,9 +6,15 @@ import AsBar from "./as";
import { useTranslations } from "next-intl";
import CeBar from "./ce";
import PeakBar from "./peak";
import { useShallow } from "zustand/react/shallow";
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 navItems = [
@@ -23,9 +29,9 @@ export default function MonsterBar() {
return (
<div className="min-h-screen">
<div className="w-full">
{/* 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">
{/* Mobile Select */}
<div className="block md:hidden w-full">
@@ -43,38 +49,48 @@ export default function MonsterBar() {
</div>
{/* Desktop Tabs */}
<div className="hidden md:grid grid-cols-3 lg:grid-cols-6 gap-1">
{navItems.map((item) => (
<button
key={item.name}
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()
? 'bg-success/30 shadow-lg'
: 'bg-base-200/20 hover:bg-base-200/40 '
<div className="hidden md:grid grid-cols-3 lg:grid-cols-6 gap-2 w-full">
{navItems.map((item) => {
const isSelected = battle_type.toUpperCase() === item.value.toUpperCase()
return (
<button
key={item.name}
onClick={() => setBattleType(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 ${
isSelected
? 'bg-success/20 border-warning shadow-lg text-base-content'
: 'bg-base-200/20 hover:bg-base-200/40 border-transparent text-base-content/80'
}`}
>
<span
style={
battle_type.toUpperCase() === item.value.toUpperCase()
? {
filter:
'brightness(0) saturate(100%) invert(63%) sepia(78%) saturate(643%) hue-rotate(1deg) brightness(93%) contrast(89%)',
}
: undefined
}
>
<Image
unoptimized
crossOrigin="anonymous"
src={`/icon/${item.icon}.webp`}
alt={item.name}
width={24}
height={24}
/>
</span>
<span>{item.name}</span>
</button>
))}
<span
className={
isSelected
? ""
: "theme-path-icon"
}
style={
isSelected
? {
filter:
'brightness(0) saturate(100%) invert(63%) sepia(78%) saturate(643%) hue-rotate(1deg) brightness(93%) contrast(89%)',
}
: undefined
}
>
<Image
unoptimized
crossOrigin="anonymous"
src={`/icon/${item.icon}.webp`}
alt={item.name}
width={24}
height={24}
className="w-5 h-5 object-contain"
/>
</span>
<span>{item.name}</span>
</button>
)
})}
</div>
</div>
</nav>
+34 -10
View File
@@ -9,14 +9,30 @@ import Image from "next/image";
import { useTranslations } from "next-intl";
import { MoCEvent, MonsterStore } from "@/types";
import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
export default function MocBar() {
const { locale } = useLocaleStore()
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const {
moc_config,
setMocConfig
} = useUserDataStore()
const { mapMonster, mapMoc, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
} = useUserDataStore(
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")
@@ -86,6 +102,14 @@ export default function MocBar() {
if (targetEventList.length > 0) {
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) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
@@ -115,10 +139,10 @@ export default function MocBar() {
if (!mapMoc) return null
return (
<div className="py-8 relative">
<div className="pt-4 pb-2 relative space-y-4">
{/* 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">
<SelectCustomText
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>
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<div className="flex items-center space-x-2 mb-2">
<div className="pt-3 border-t border-base-content/10 space-y-2">
<div className="flex items-center space-x-2">
<input
type="checkbox"
checked={moc_config.use_turbulence_buff}
@@ -215,7 +239,7 @@ export default function MocBar() {
challengeSelected.MazeBuff.map((buff, i) => (
<div
key={i}
className="text-base"
className="text-sm text-base-content/85 leading-relaxed"
dangerouslySetInnerHTML={{
__html: replaceByParam(
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>
@@ -246,7 +270,7 @@ export default function MocBar() {
const targetEvent = eventList[0];
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>
{targetEvent?.MonsterList?.map((wave, waveIndex) => (
+378 -286
View File
@@ -1,308 +1,400 @@
"use client"
"use client";
import { useEffect, useMemo } from "react";
import SelectCustomText from "../select/customSelectText";
import { calcMonsterStats, getLocaleName, replaceByParam } from "@/helper";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";;
import useUserDataStore from "@/stores/userDataStore";
import Image from "next/image";
import { useTranslations } from "next-intl";
import { MonsterStore } from "@/types";
import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
export default function PeakBar() {
const { locale } = useLocaleStore()
const {
peak_config,
setPeakConfig
} = useUserDataStore()
const { mapMonster, mapPeak, damageType, eliteConfig, hardLevelConfig } = useDetailDataStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale })),
);
const { peak_config, setPeakConfig } = useUserDataStore(
useShallow((state) => ({
peak_config: state.peak_config,
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 peak = mapPeak?.[peak_config?.event_id?.toString()]
if (!peak) return []
const listFloor = useMemo(() => {
const peak = mapPeak?.[peak_config?.event_id?.toString()];
if (!peak) return [];
return [...peak.PreLevel, peak.BossLevel].filter(it => it != null)
}, [peak_config, mapPeak])
const eventSelected = useMemo(() => {
return mapPeak?.[peak_config?.event_id?.toString()]
}, [peak_config, mapPeak])
return [...peak.PreLevel, peak.BossLevel].filter((it) => it != null);
}, [peak_config, mapPeak]);
const eventSelected = useMemo(() => {
return mapPeak?.[peak_config?.event_id?.toString()];
}, [peak_config, mapPeak]);
const bossConfig = useMemo(() => {
return mapPeak?.[peak_config?.event_id?.toString()]?.BossConfig;
}, [peak_config, mapPeak])
const bossConfig = useMemo(() => {
return mapPeak?.[peak_config?.event_id?.toString()]?.BossConfig;
}, [peak_config, mapPeak]);
const challengeSelected = useMemo(() => {
const challenge = structuredClone(listFloor?.find((peak) => peak?.ID === peak_config.challenge_id))
if (
challenge
&& challenge.ID === mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID
&& bossConfig
&& peak_config?.boss_mode === "Hard"
) {
return { challenge: bossConfig, isBoss: true }
const challengeSelected = useMemo(() => {
const challenge = structuredClone(
listFloor?.find((peak) => peak?.ID === peak_config.challenge_id),
);
if (
challenge &&
challenge.ID ===
mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID &&
bossConfig &&
peak_config?.boss_mode === "Hard"
) {
return { challenge: bossConfig, isBoss: true };
}
return {
challenge: challenge,
isBoss:
challenge?.ID ===
mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID,
};
}, [peak_config, listFloor, mapPeak, bossConfig]);
useEffect(() => {
if (!challengeSelected.challenge) return;
if (
peak_config.event_id !== 0 &&
peak_config.challenge_id !== 0 &&
challengeSelected
) {
const newBattleConfig = structuredClone(peak_config);
newBattleConfig.cycle_count = 6;
newBattleConfig.blessings = [];
for (const value of challengeSelected?.challenge?.MazeBuff) {
newBattleConfig.blessings.push({
id: value.ID,
level: 1,
});
}
if (peak_config.buff_id !== 0 && challengeSelected.isBoss) {
newBattleConfig.blessings.push({
id: peak_config.buff_id,
level: 1,
});
}
newBattleConfig.monsters = [];
newBattleConfig.stage_id = challengeSelected.challenge.EventList[0].ID;
if (challengeSelected.challenge.EventList[0].Invasion) {
console.log("Adding Invasion Buff", challengeSelected.challenge.EventList[0].Invasion.MazeBuff.ID);
newBattleConfig.blessings.push({
id: challengeSelected.challenge.EventList[0].Invasion.MazeBuff.ID,
level: 1,
});
}
for (const wave of challengeSelected.challenge.EventList[0].MonsterList) {
if (!wave) continue;
const newWave: MonsterStore[] = [];
for (const value of Object.values(wave)) {
if (!value) continue;
newWave.push({
monster_id: value,
level: challengeSelected.challenge.EventList[0].Level,
amount: 1,
});
}
return {
challenge: challenge,
isBoss: challenge?.ID === mapPeak?.[peak_config?.event_id?.toString()]?.BossLevel?.ID,
}
}, [peak_config, listFloor, mapPeak, bossConfig])
newBattleConfig.monsters.push(newWave);
}
useEffect(() => {
if (!challengeSelected.challenge) return
if (peak_config.event_id !== 0 && peak_config.challenge_id !== 0 && challengeSelected) {
const newBattleConfig = structuredClone(peak_config)
newBattleConfig.cycle_count = 6
newBattleConfig.blessings = []
for (const value of challengeSelected?.challenge?.MazeBuff) {
newBattleConfig.blessings.push({
id: value.ID,
level: 1
})
setPeakConfig(newBattleConfig);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
peak_config.event_id,
peak_config.challenge_id,
peak_config.buff_id,
peak_config.boss_mode,
mapPeak,
]);
if (!mapPeak) return null;
return (
<div className="pt-4 pb-2 relative space-y-4">
{/* Title Card */}
<div className="rounded-xl p-4 border border-warning">
<div className="mb-4 w-full">
<SelectCustomText
customSet={Object.values(mapPeak)
.sort((a, b) => b.ID - a.ID)
.map((peak) => ({
id: peak.ID.toString(),
name: `${getLocaleName(locale, peak.Name)} (${peak.ID})`,
}))}
excludeSet={[]}
selectedCustomSet={peak_config.event_id.toString()}
placeholder={transI18n("selectPEAKEvent")}
setSelectedCustomSet={(id) =>
setPeakConfig({
...peak_config,
event_id: Number(id),
challenge_id: 0,
buff_id: 0,
})
}
if (peak_config.buff_id !== 0 && challengeSelected.isBoss) {
newBattleConfig.blessings.push({
id: peak_config.buff_id,
level: 1
})
}
newBattleConfig.monsters = []
newBattleConfig.stage_id = challengeSelected.challenge.EventList[0].ID
for (const wave of challengeSelected.challenge.EventList[0].MonsterList) {
if (!wave) continue
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
if (!value) continue
newWave.push({
monster_id: value,
level: challengeSelected.challenge.EventList[0].Level,
amount: 1,
})
}
newBattleConfig.monsters.push(newWave)
}
setPeakConfig(newBattleConfig)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
peak_config.event_id,
peak_config.challenge_id,
peak_config.buff_id,
peak_config.boss_mode,
mapPeak,
])
if (!mapPeak) return null
return (
<div className="py-8 relative">
{/* Title Card */}
<div className="rounded-xl p-4 mb-2 border border-warning">
<div className="mb-4 w-full">
<SelectCustomText
customSet={Object.values(mapPeak).sort((a, b) => b.ID - a.ID).map((peak) => ({
id: peak.ID.toString(),
name: `${getLocaleName(locale, peak.Name)} (${peak.ID})`,
}))}
excludeSet={[]}
selectedCustomSet={peak_config.event_id.toString()}
placeholder={transI18n("selectPEAKEvent")}
setSelectedCustomSet={(id) => setPeakConfig({ ...peak_config, event_id: Number(id), challenge_id: 0, buff_id: 0 })}
/>
</div>
{/* Settings */}
<div className={
`grid grid-cols-1
/>
</div>
{/* Settings */}
<div
className={`grid grid-cols-1
${eventSelected && eventSelected.BossLevel?.ID === peak_config.challenge_id ? "md:grid-cols-2" : ""}
gap-4 mb-2 justify-items-center items-center w-full`}
>
<div className="flex items-center gap-2 w-full">
<label className="label">
<span className="label-text font-bold text-success">
{transI18n("floor")}:{" "}
</span>
</label>
<select
value={peak_config.challenge_id}
className="select select-success w-full"
onChange={(e) =>
setPeakConfig({
...peak_config,
challenge_id: Number(e.target.value),
})
}
>
<option value={0} disabled={true}>
{transI18n("selectFloor")}
</option>
{listFloor.map((peak) => (
<option key={peak.ID} value={peak.ID}>
{getLocaleName(locale, peak.Name)}
</option>
))}
</select>
</div>
{eventSelected &&
eventSelected.BossLevel?.ID === peak_config.challenge_id && (
<div className="flex items-center gap-2 w-full">
<label className="label">
<span className="label-text font-bold text-success">
{transI18n("mode")}:{" "}
</span>
</label>
<select
value={peak_config.boss_mode}
className="select select-success w-full"
onChange={(e) =>
setPeakConfig({ ...peak_config, boss_mode: e.target.value })
}
>
<div className="flex items-center gap-2 w-full">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("floor")}:{" "}</span>
</label>
<select
value={peak_config.challenge_id}
className="select select-success w-full"
onChange={(e) => setPeakConfig({ ...peak_config, challenge_id: Number(e.target.value) })}
>
<option value={0} disabled={true}>{transI18n("selectFloor")}</option>
{listFloor.map((peak) => (
<option key={peak.ID} value={peak.ID}>{getLocaleName(locale, peak.Name)}</option>
))}
</select>
</div>
{eventSelected && eventSelected.BossLevel?.ID === peak_config.challenge_id && (
<div className="flex items-center gap-2 w-full">
<label className="label">
<span className="label-text font-bold text-success">{transI18n("mode")}:{" "}</span>
</label>
<select
value={peak_config.boss_mode}
className="select select-success w-full"
onChange={(e) => setPeakConfig({ ...peak_config, boss_mode: e.target.value })}
>
<option value={0} disabled={true}>{transI18n("selectSide")}</option>
<option value="Normal">{transI18n("normalMode")}</option>
<option value="Hard">{transI18n("hardMode")}</option>
</select>
</div>
)}
</div>
<div className="label-text font-bold text-success mb-2">StageId: {peak_config?.stage_id}</div>
{
eventSelected
&& eventSelected.BossLevel?.ID === peak_config.challenge_id
&& bossConfig
&& (
<div className="mb-4 w-full">
<SelectCustomText
customSet={
bossConfig.BuffList.map((buff) => ({
id: buff.ID.toString(),
name: getLocaleName(locale, buff?.Name || ""),
description: replaceByParam(getLocaleName(locale, buff?.Desc || ""), buff?.Param || []),
}))
}
excludeSet={[]}
selectedCustomSet={peak_config?.buff_id?.toString()}
placeholder={transI18n("selectBuff")}
setSelectedCustomSet={(id) => setPeakConfig({ ...peak_config, buff_id: Number(id) })}
/>
</div>
)
}
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<h2 className="text-2xl font-bold mb-2 text-info">
{transI18n("turbulenceBuff")}
</h2>
{challengeSelected?.challenge && challengeSelected?.challenge?.MazeBuff?.length > 0 ? (
challengeSelected.challenge.MazeBuff.map((subOption, index) => (
<div key={index}>
<label className="label">
<span className="label-text font-bold text-success">
{index + 1}. {getLocaleName(locale, subOption.Name)}
</span>
</label>
<div
className="text-base"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, subOption.Desc),
subOption.Param || []
)
}}
/>
</div>
))
) : (
<div className="text-base">{transI18n("noTurbulenceBuff")}</div>
)}
</div>
</div>
{/* Enemy Waves */}
{(peak_config?.challenge_id ?? 0) !== 0 && (
<div className="grid grid-cols-1 gap-4">
<div className="rounded-xl p-4 mt-2 border border-warning">
<h2 className="text-2xl font-bold mb-2 text-info">{getLocaleName(locale, challengeSelected?.challenge?.Name)}</h2>
{challengeSelected?.challenge && Object.values(challengeSelected?.challenge?.EventList?.[0]?.Infinite || []).map((waveValue, waveIndex) => (
<div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold">{transI18n("wave")} {waveIndex + 1}</h3>
<div className="flex flex-wrap gap-2 mt-2">
{Array.from(new Set(waveValue.MonsterList)).map((monsterId, enemyIndex) => {
const monsterStats = calcMonsterStats(
mapMonster?.[monsterId.toString()],
waveValue.EliteGroup,
challengeSelected?.challenge?.EventList?.[0]?.HardLevelGroup || 0,
challengeSelected?.challenge?.EventList?.[0]?.Level || 0,
hardLevelConfig,
eliteConfig
);
return (
<div
key={enemyIndex}
className="group relative flex flex-col w-40 bg-base-100 rounded-2xl border border-base-300 shadow-md"
>
<div className="badge badge-warning badge-sm font-bold absolute top-2 right-2 z-10 shadow-sm">
Lv. {challengeSelected?.challenge?.EventList?.[0]?.Level}
</div>
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
{mapMonster?.[monsterId.toString()]?.Image?.IconPath && (
<div className="relative w-16 h-16 rounded-full border-2 border-base-300 shadow-md overflow-hidden group-hover:scale-110 transition-transform duration-300 bg-base-100">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${mapMonster?.[monsterId.toString()]?.Image?.IconPath}`}
alt="Enemy Icon"
width={150}
height={150}
className="w-full h-full object-cover"
/>
</div>
)}
</div>
<div className="flex flex-col px-1 pb-2 pt-2">
<div className="flex flex-col space-y-1.5">
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
<span className="text-xs font-semibold text-error">HP</span>
<span className="text-sm font-bold text-base-content">{monsterStats.hp.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
</div>
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
<span className="text-xs font-semibold text-info">Speed</span>
<span className="text-sm font-bold text-base-content">{monsterStats.spd.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
</div>
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
<span className="text-xs font-semibold text-base-content/70">Toughness</span>
<span className="text-sm font-bold text-base-content">{monsterStats.stance.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
</div>
</div>
<div className="mt-2 pt-2 border-t border-base-300 flex flex-col items-center">
<span className="text-[10px] text-base-content/60 font-bold uppercase tracking-widest mb-1.5">
Weakness
</span>
<div className="flex items-center justify-center gap-1.5 flex-wrap">
{mapMonster?.[monsterId.toString()]?.StanceWeakList?.map((icon, iconIndex) => (
<Image
key={iconIndex}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType[icon]?.Icon}`}
alt={icon}
width={40}
height={40}
className="h-6 w-6 object-contain rounded-full bg-base-300 border border-base-content/10 p-0.5 shadow-sm hover:scale-110 transition-transform"
/>
))}
</div>
</div>
</div>
</div>
)
})}
</div>
</div>
))}
</div>
</div>
<option value={0} disabled={true}>
{transI18n("selectSide")}
</option>
<option value="Normal">{transI18n("normalMode")}</option>
<option value="Hard">{transI18n("hardMode")}</option>
</select>
</div>
)}
</div>
)
}
<div className="label-text font-bold text-success mb-2">
StageId: {peak_config?.stage_id}
</div>
{eventSelected &&
eventSelected.BossLevel?.ID === peak_config.challenge_id &&
bossConfig && (
<div className="mb-4 w-full">
<SelectCustomText
customSet={bossConfig.BuffList.map((buff) => ({
id: buff.ID.toString(),
name: getLocaleName(locale, buff?.Name || ""),
description: replaceByParam(
getLocaleName(locale, buff?.Desc || ""),
buff?.Param || [],
),
}))}
excludeSet={[]}
selectedCustomSet={peak_config?.buff_id?.toString()}
placeholder={transI18n("selectBuff")}
setSelectedCustomSet={(id) =>
setPeakConfig({ ...peak_config, buff_id: Number(id) })
}
/>
</div>
)}
{/* Turbulence Buff */}
<div className="pt-3 border-t border-base-content/10 space-y-2">
<h2 className="text-xl font-bold text-info">
{transI18n("turbulenceBuff")}
</h2>
{challengeSelected?.challenge &&
challengeSelected?.challenge?.MazeBuff?.length > 0 ? (
challengeSelected.challenge.MazeBuff.map((subOption, index) => (
<div key={index} className="space-y-1">
<span className="font-bold text-success text-sm">
{index + 1}. {getLocaleName(locale, subOption.Name)}
</span>
<div
className="text-sm text-base-content/85 leading-relaxed"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, subOption.Desc),
subOption.Param || [],
),
}}
/>
</div>
))
) : (
<div className="text-sm text-base-content/50 italic">
{transI18n("noTurbulenceBuff")}
</div>
)}
</div>
</div>
{/* Enemy Waves */}
{(peak_config?.challenge_id ?? 0) !== 0 && (
<div className="grid grid-cols-1 gap-4">
<div className="rounded-xl p-4 border border-warning">
<h2 className="text-2xl font-bold mb-2 text-info">
{getLocaleName(locale, challengeSelected?.challenge?.Name)}
</h2>
{challengeSelected?.challenge &&
Object.values(
challengeSelected?.challenge?.EventList?.[0]?.Infinite || [],
).map((waveValue, waveIndex) => (
<div key={waveIndex} className="mb-6">
<h3 className="text-lg font-semibold">
{transI18n("wave")} {waveIndex + 1}
</h3>
<div className="flex flex-wrap gap-2 mt-2">
{Array.from(new Set(waveValue.MonsterList)).map(
(monsterId, enemyIndex) => {
const monsterStats = calcMonsterStats(
mapMonster?.[monsterId.toString()],
waveValue.EliteGroup,
challengeSelected?.challenge?.EventList?.[0]
?.HardLevelGroup || 0,
challengeSelected?.challenge?.EventList?.[0]?.Level ||
0,
hardLevelConfig,
eliteConfig,
);
return (
<div
key={enemyIndex}
className="group relative flex flex-col w-40 bg-base-100 rounded-2xl border border-base-300 shadow-md"
>
<div className="badge badge-warning badge-sm font-bold absolute top-2 right-2 z-10 shadow-sm">
Lv.{" "}
{
challengeSelected?.challenge?.EventList?.[0]
?.Level
}
</div>
<div className="relative w-full h-20 bg-base-200 flex items-center justify-center p-4 rounded-t-2xl">
{mapMonster?.[monsterId.toString()]?.Image
?.IconPath && (
<div className="relative w-16 h-16 rounded-full border-2 border-base-300 shadow-md overflow-hidden group-hover:scale-110 transition-transform duration-300 bg-base-100">
<Image
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${mapMonster?.[monsterId.toString()]?.Image?.IconPath}`}
alt="Enemy Icon"
width={150}
height={150}
className="w-full h-full object-cover"
/>
</div>
)}
</div>
<div className="flex flex-col px-1 pb-2 pt-2">
<div className="flex flex-col space-y-1.5">
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
<span className="text-xs font-semibold text-error">
HP
</span>
<span className="text-sm font-bold text-base-content">
{monsterStats.hp.toLocaleString(undefined, {
maximumFractionDigits: 0,
})}
</span>
</div>
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
<span className="text-xs font-semibold text-info">
Speed
</span>
<span className="text-sm font-bold text-base-content">
{monsterStats.spd.toLocaleString(
undefined,
{ maximumFractionDigits: 0 },
)}
</span>
</div>
<div className="flex justify-between items-center bg-base-200 px-2.5 py-1.5 rounded-lg">
<span className="text-xs font-semibold text-base-content/70">
Toughness
</span>
<span className="text-sm font-bold text-base-content">
{monsterStats.stance.toLocaleString(
undefined,
{ maximumFractionDigits: 0 },
)}
</span>
</div>
</div>
<div className="mt-2 pt-2 border-t border-base-300 flex flex-col items-center">
<span className="text-[10px] text-base-content/60 font-bold uppercase tracking-widest mb-1.5">
Weakness
</span>
<div className="flex items-center justify-center gap-1.5 flex-wrap">
{mapMonster?.[
monsterId.toString()
]?.StanceWeakList?.map((icon, iconIndex) => (
<Image
key={iconIndex}
unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${damageType[icon]?.Icon}`}
alt={icon}
width={40}
height={40}
className="h-6 w-6 object-contain rounded-full bg-base-300 border border-base-content/10 p-0.5 shadow-sm hover:scale-110 transition-transform"
/>
))}
</div>
</div>
</div>
</div>
);
},
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
);
}
+37 -16
View File
@@ -8,14 +8,30 @@ import Image from "next/image";
import { MonsterStore, PFEvent } from "@/types";
import { useTranslations } from "next-intl";
import useDetailDataStore from "@/stores/detailDataStore";
import { useShallow } from "zustand/react/shallow";
export default function PfBar() {
const { locale } = useLocaleStore()
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
const {
pf_config,
setPfConfig
} = useUserDataStore()
const { mapMonster, mapPF, damageType, hardLevelConfig, eliteConfig } = useDetailDataStore()
} = useUserDataStore(
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 challengeSelected = useMemo(() => {
@@ -89,6 +105,14 @@ export default function PfBar() {
if (targetEventList.length > 0) {
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) {
const newWave: MonsterStore[] = []
for (const value of Object.values(wave)) {
@@ -116,10 +140,10 @@ export default function PfBar() {
if (!mapPF) return null
return (
<div className="py-8 relative">
<div className="pt-4 pb-2 relative space-y-4">
{/* 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">
<SelectCustomText
customSet={Object.values(mapPF).sort((a, b) => b.ID - a.ID).map((pf) => ({
@@ -190,16 +214,14 @@ export default function PfBar() {
</div>
)}
{/* Turbulence Buff */}
<div className="bg-base-200/20 rounded-lg p-4 border border-purple-500/20">
<h2 className="text-2xl font-bold mb-2 text-info">{transI18n("turbulenceBuff")}</h2>
<div className="pt-3 border-t border-base-content/10 space-y-2">
<h2 className="text-xl font-bold text-info">{transI18n("turbulenceBuff")}</h2>
{eventSelected && eventSelected.SubOption.length > 0 ? (
eventSelected.SubOption.map((subOption, index) => (
<div key={index}>
<label className="label">
<span className="label-text font-bold text-success">{index + 1}. {getLocaleName(locale, subOption.Name)}</span>
</label>
<div key={index} className="space-y-1">
<span className="font-bold text-success text-sm">{index + 1}. {getLocaleName(locale, subOption.Name)}</span>
<div
className="text-base"
className="text-sm text-base-content/85 leading-relaxed"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, subOption.Desc) || "",
@@ -213,7 +235,7 @@ export default function PfBar() {
challengeSelected?.MazeBuff?.map((buff, i) => (
<div
key={i}
className="text-base"
className="text-sm text-base-content/85 leading-relaxed"
dangerouslySetInnerHTML={{
__html: replaceByParam(
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>
{/* Enemy Waves */}
{(pf_config?.challenge_id ?? 0) !== 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{floorSideList.map((side, i) => {
@@ -245,7 +266,7 @@ export default function PfBar() {
const targetEvent = eventList[0];
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>
{targetEvent && Object.values(targetEvent.Infinite || []).map((waveValue, waveIndex) => (
+15 -15
View File
@@ -1,16 +1,16 @@
'use client'
import { parseRuby } from "@/helper";
interface TextProps {
text: string;
locale: string;
className?: string;
}
export default function ParseText({ text, locale, className }: TextProps) {
if (locale === "ja") {
return <div className={className} dangerouslySetInnerHTML={{ __html: parseRuby(text) }} />;
}
return <div className={className}>{text}</div>;
'use client'
import { parseRuby } from "@/helper";
interface TextProps {
text: string;
locale: string;
className?: string;
}
export default function ParseText({ text, locale, className }: TextProps) {
if (locale === "ja") {
return <div className={className} dangerouslySetInnerHTML={{ __html: parseRuby(text) }} />;
}
return <div className={className}>{text}</div>;
}
+11 -11
View File
@@ -1,12 +1,12 @@
"use client";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React from 'react';
export default function QueryProviderWrapper({ children }: { children: React.ReactNode }) {
const [queryClient] = React.useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
"use client";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import React from 'react';
export default function QueryProviderWrapper({ children }: { children: React.ReactNode }) {
const [queryClient] = React.useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
}
+505 -472
View File
@@ -1,473 +1,506 @@
"use client"
import NextImage from "next/image"
import useUserDataStore from "@/stores/userDataStore";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { calcAffixBonus, calcBaseStatRaw, calcBonusStatRaw, calcMainAffixBonus, calcMainAffixBonusRaw, calcPromotion, calcSubAffixBonusRaw, getLocaleName, replaceByParam } from "@/helper";
import { mappingStats } from "@/constant/constant";
import RelicShowcase from "../showcaseCard/relicShowcase";
import useLocaleStore from "@/stores/localeStore";
import useDetailDataStore from "@/stores/detailDataStore";
import useCurrentDataStore from "@/stores/currentDataStore";
export default function QuickView() {
const { avatars } = useUserDataStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore()
const { avatarSelected, } = useCurrentDataStore()
const { mainAffix, subAffix, mapRelicSet, mapLightCone, mapAvatar } = useDetailDataStore()
const avatarSkillTree = useMemo(() => {
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
if (avatars[avatarSelected?.ID?.toString()].enhanced) {
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
}
return avatarSelected?.SkillTrees || {}
}, [avatarSelected, avatars])
const avatarData = useMemo(() => {
if (!avatarSelected) return
return avatars[avatarSelected?.ID?.toString()]
}, [avatarSelected, avatars])
const avatarProfile = useMemo(() => {
if (!avatarSelected || !avatarData) return
return avatarData?.profileList?.[avatarData?.profileSelect]
}, [avatarSelected, avatarData])
const relicEffects = useMemo(() => {
const avatar = avatars[avatarSelected?.ID?.toString() || ""];
const relicCount: { [key: string]: number } = {};
if (avatar) {
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
if (relicCount[relic.relic_set_id]) {
relicCount[relic.relic_set_id]++;
} else {
relicCount[relic.relic_set_id] = 1;
}
}
}
const listEffects: { key: string, count: number }[] = [];
Object.entries(relicCount).forEach(([key, value]) => {
if (value >= 2) {
listEffects.push({ key: key, count: value });
}
});
return listEffects;
}, [avatars, avatarSelected]);
const relicStats = useMemo(() => {
if (!avatarSelected || !avatarProfile?.relics || !mainAffix || !subAffix) return
return Object.entries(avatarProfile?.relics).map(([key, value]) => {
const mainAffixMap = mainAffix["5" + key]
const subAffixMap = subAffix["5"]
if (!mainAffixMap || !subAffixMap) return
return {
img: `${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${value.relic_set_id}_${key}.png`,
mainAffix: {
property: mainAffixMap?.[value?.main_affix_id]?.Property,
level: value?.level,
valueAffix: calcMainAffixBonus(mainAffixMap?.[value?.main_affix_id], value?.level),
detail: mappingStats?.[mainAffixMap?.[value?.main_affix_id]?.Property]
},
subAffix: value?.sub_affixes?.map((subValue) => {
return {
property: subAffixMap?.[subValue?.sub_affix_id]?.Property,
valueAffix: calcAffixBonus(subAffixMap?.[subValue?.sub_affix_id], subValue?.step, subValue?.count),
detail: mappingStats?.[subAffixMap?.[subValue?.sub_affix_id]?.Property],
step: subValue?.step,
count: subValue?.count
}
})
}
})
}, [avatarSelected, avatarProfile, mainAffix, subAffix])
const characterStats = useMemo(() => {
if (!avatarSelected || !avatarData) return
const charPromotion = calcPromotion(avatarData.level)
const statsData: Record<string, {
value: number,
base: number,
name: string,
icon: string,
unit: string,
round: number
}> = {
HP: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
avatarData.level
),
name: "HP",
icon: "spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "",
round: 0
},
ATK: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
avatarData.level
),
name: "ATK",
icon: "spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "",
round: 0
},
DEF: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
avatarData.level
),
name: "DEF",
icon: "spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "",
round: 0
},
SPD: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
name: "SPD",
icon: "spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
round: 1
},
CRITRate: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
name: "CRIT Rate",
icon: "spriteoutput/ui/avatar/icon/IconCriticalChance.png",
unit: "%",
round: 1
},
CRITDmg: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
name: "CRIT DMG",
icon: "spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
unit: "%",
round: 1
},
BreakEffect: {
value: 0,
base: 0,
name: "Break Effect",
icon: "spriteoutput/ui/avatar/icon/IconBreakUp.png",
unit: "%",
round: 1
},
EffectRES: {
value: 0,
base: 0,
name: "Effect RES",
icon: "spriteoutput/ui/avatar/icon/IconStatusResistance.png",
unit: "%",
round: 1
},
EnergyRate: {
value: 0,
base: 0,
name: "Energy Rate",
icon: "spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
unit: "%",
round: 1
},
EffectHitRate: {
value: 0,
base: 0,
name: "Effect Hit Rate",
icon: "spriteoutput/ui/avatar/icon/IconStatusProbability.png",
unit: "%",
round: 1
},
HealBoost: {
value: 0,
base: 0,
name: "Healing Boost",
icon: "spriteoutput/ui/avatar/icon/IconHealRatio.png",
unit: "%",
round: 1
},
PhysicalAdd: {
value: 0,
base: 0,
name: "Physical Boost",
icon: "spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
unit: "%",
round: 1
},
FireAdd: {
value: 0,
base: 0,
name: "Fire Boost",
icon: "spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
unit: "%",
round: 1
},
IceAdd: {
value: 0,
base: 0,
name: "Ice Boost",
icon: "spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
unit: "%",
round: 1
},
ThunderAdd: {
value: 0,
base: 0,
name: "Thunder Boost",
icon: "spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
unit: "%",
round: 1
},
WindAdd: {
value: 0,
base: 0,
name: "Wind Boost",
icon: "spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
unit: "%",
round: 1
},
QuantumAdd: {
value: 0,
base: 0,
name: "Quantum Boost",
icon: "spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
unit: "%",
round: 1
},
ImaginaryAdd: {
value: 0,
base: 0,
name: "Imaginary Boost",
icon: "spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
unit: "%",
round: 1
},
ElationAdd: {
value: 0,
base: 0,
name: "Elation Boost",
icon: "spriteoutput/ui/avatar/icon/IconJoy.png",
unit: "%",
round: 1
}
}
if (avatarProfile?.lightcone && mapLightCone[avatarProfile?.lightcone?.item_id]) {
const lightconePromotion = calcPromotion(avatarProfile?.lightcone?.level)
statsData.HP.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd,
avatarProfile?.lightcone?.level
)
statsData.HP.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHPAdd,
avatarProfile?.lightcone?.level
)
statsData.ATK.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
avatarProfile?.lightcone?.level
)
statsData.ATK.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
avatarProfile?.lightcone?.level
)
statsData.DEF.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
avatarProfile?.lightcone?.level
)
statsData.DEF.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
avatarProfile?.lightcone?.level
)
const bonusData = mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Skills?.Level?.[avatarProfile?.lightcone.rank]?.Bonus
if (bonusData && bonusData.length > 0) {
const bonusSpd = bonusData.filter((bonus) => bonus.PropertyType === "BaseSpeed")
const bonusOther = bonusData.filter((bonus) => bonus.PropertyType !== "BaseSpeed")
bonusSpd.forEach((bonus) => {
statsData.SPD.value += bonus.Value
statsData.SPD.base += bonus.Value
})
bonusOther.forEach((bonus) => {
const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
}
})
}
}
if (avatarSkillTree) {
Object.values(avatarSkillTree).forEach((value) => {
if (value?.["1"]
&& value?.["1"]?.PointID
&& typeof avatarData?.data?.skills?.[value?.["1"]?.PointID] === "number"
&& avatarData?.data?.skills?.[value?.["1"]?.PointID] !== 0
&& value?.["1"]?.StatusAddList
&& value?.["1"].StatusAddList.length > 0) {
value?.["1"]?.StatusAddList.forEach((status) => {
const statsBase = mappingStats?.[status?.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(status?.PropertyType, statsData[statsBase].base, status.Value)
}
})
}
})
}
if (avatarProfile?.relics && mainAffix && subAffix) {
Object.entries(avatarProfile?.relics).forEach(([key, value]) => {
const mainAffixMap = mainAffix["5" + key]
const subAffixMap = subAffix["5"]
if (!mainAffixMap || !subAffixMap) return
const mainStats = mappingStats?.[mainAffixMap?.[value.main_affix_id]?.Property]?.baseStat
if (mainStats && statsData[mainStats]) {
statsData[mainStats].value += calcMainAffixBonusRaw(mainAffixMap?.[value.main_affix_id], value.level, statsData[mainStats].base)
}
value?.sub_affixes.forEach((subValue) => {
const subStats = mappingStats?.[subAffixMap?.[subValue.sub_affix_id]?.Property]?.baseStat
if (subStats && statsData[subStats]) {
statsData[subStats].value += calcSubAffixBonusRaw(subAffixMap?.[subValue.sub_affix_id], subValue.step, subValue.count, statsData[subStats].base)
}
})
})
}
if (relicEffects && relicEffects.length > 0) {
relicEffects.forEach((relic) => {
const dataBonus = mapRelicSet?.[relic.key]?.Skills
if (!dataBonus || Object.keys(dataBonus).length === 0) return
Object.entries(dataBonus || {}).forEach(([key, value]) => {
if (relic.count < Number(key)) return
value.Bonus.forEach((bonus) => {
const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
}
})
})
})
}
return statsData
}, [
avatarSelected,
avatarData,
mapAvatar,
avatarProfile?.lightcone,
avatarProfile?.relics,
mapLightCone,
mainAffix,
subAffix,
relicEffects,
mapRelicSet,
avatarSkillTree
])
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="col-span-1 md:col-span-2 flex flex-col justify-between py-3">
<div className="flex w-full flex-col justify-between gap-y-0.5 text-base">
{Object.entries(characterStats || {})?.map(([key, stat], index) => {
if (!stat || (key.includes("Add") && stat.value === 0)) return null
return (
<div key={index} className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<NextImage
src={`${process.env.CDN_URL}/${stat?.icon}`}
unoptimized
crossOrigin="anonymous"
alt="Stat Icon"
width={40}
height={40}
className="h-10 w-10 p-1 mx-1 bg-black/20 rounded-full"
/>
<div className="font-bold">{stat.name}</div>
</div>
<div className="ml-3 mr-3 grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">{
stat.value ? stat.unit === "%" ? (stat.value * 100).toFixed(stat.round) : stat.value.toFixed(stat.round) : 0
}{stat.unit}</div>
</div>
)
})}
<hr />
</div>
<div className="flex flex-col items-center gap-1 w-full my-2">
{relicEffects.map((setEffect, index) => {
const relicInfo = mapRelicSet[setEffect.key];
if (!relicInfo) return null;
return (
<div key={index} className="flex w-full flex-row justify-between text-left">
<div
className="font-bold truncate max-w-full mr-1"
style={{
fontSize: 'clamp(0.5rem, 2vw, 1rem)'
}}
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, relicInfo.Name),
[]
)
}}
/>
<div>
<span className="black-blur bg-black/20 flex w-5 justify-center rounded px-1.5 py-0.5">{setEffect.count}</span>
</div>
</div>
)
})}
</div>
</div>
<div className="grid grid-cols-1 gap-2 justify-between py-3 text-lg">
{relicStats?.map((relic, index) => {
if (!relic || !avatarSelected) return null
return (
<RelicShowcase key={index} relic={relic} avatarInfo={avatarSelected} />
)
})}
{(!relicStats || !relicStats?.length) && (
<div className="flex flex-col items-center justify-center">
<div className="text-center p-6 rounded-lg bg-black/40 backdrop-blur-sm border border-white/10">
<span className="text-lg text-gray-400">{transI18n("noRelicEquipped")}</span>
</div>
</div>
)}
</div>
</div>
)
"use client"
import NextImage from "next/image"
import useUserDataStore from "@/stores/userDataStore";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { calcAffixBonus, calcBaseStatRaw, calcBonusStatRaw, calcMainAffixBonus, calcMainAffixBonusRaw, calcPromotion, calcSubAffixBonusRaw, getLocaleName, replaceByParam } from "@/helper";
import { mappingStats } from "@/constant/constant";
import RelicShowcase from "../showcaseCard/relicShowcase";
import useLocaleStore from "@/stores/localeStore";
import useDetailDataStore from "@/stores/detailDataStore";
import useCurrentDataStore from "@/stores/currentDataStore";
import { useShallow } from "zustand/react/shallow";
export default function QuickView() {
const { avatars } = useUserDataStore(
useShallow((state) => ({ avatars: state.avatars }))
)
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore(
useShallow((state) => ({ locale: state.locale }))
)
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(() => {
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
if (avatars[avatarSelected?.ID?.toString()].enhanced) {
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
}
return avatarSelected?.SkillTrees || {}
}, [avatarSelected, avatars])
const avatarData = useMemo(() => {
if (!avatarSelected) return
return avatars[avatarSelected?.ID?.toString()]
}, [avatarSelected, avatars])
const avatarProfile = useMemo(() => {
if (!avatarSelected || !avatarData) return
return avatarData?.profileList?.[avatarData?.profileSelect] ?? avatarData?.profileList?.[0]
}, [avatarSelected, avatarData])
const relicEffects = useMemo(() => {
const avatar = avatars[avatarSelected?.ID?.toString() || ""];
const relicCount: { [key: string]: number } = {};
const profile = avatar?.profileList?.[avatar?.profileSelect] ?? avatar?.profileList?.[0];
if (profile?.relics) {
for (const relic of Object.values(profile.relics)) {
if (relic && relic.relic_set_id) {
relicCount[relic.relic_set_id] = (relicCount[relic.relic_set_id] || 0) + 1;
}
}
}
const listEffects: { key: string, count: number }[] = [];
Object.entries(relicCount).forEach(([key, value]) => {
if (value >= 2) {
listEffects.push({ key: key, count: value });
}
});
return listEffects;
}, [avatars, avatarSelected]);
const relicStats = useMemo(() => {
if (!avatarSelected || !avatarProfile?.relics || !mainAffix || !subAffix) return
return Object.entries(avatarProfile?.relics).map(([key, value]) => {
const mainAffixMap = mainAffix["5" + key]
const subAffixMap = subAffix["5"]
if (!mainAffixMap || !subAffixMap) return
return {
img: `${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${value.relic_set_id}_${key}.png`,
mainAffix: {
property: mainAffixMap?.[value?.main_affix_id]?.Property,
level: value?.level,
valueAffix: calcMainAffixBonus(mainAffixMap?.[value?.main_affix_id], value?.level),
detail: mappingStats?.[mainAffixMap?.[value?.main_affix_id]?.Property]
},
subAffix: value?.sub_affixes?.map((subValue) => {
return {
property: subAffixMap?.[subValue?.sub_affix_id]?.Property,
valueAffix: calcAffixBonus(subAffixMap?.[subValue?.sub_affix_id], subValue?.step, subValue?.count),
detail: mappingStats?.[subAffixMap?.[subValue?.sub_affix_id]?.Property],
step: subValue?.step,
count: subValue?.count
}
})
}
})
}, [avatarSelected, avatarProfile, mainAffix, subAffix])
const characterStats = useMemo(() => {
if (!avatarSelected || !avatarData) return
const charPromotion = calcPromotion(avatarData.level)
const statsData: Record<string, {
value: number,
base: number,
name: string,
icon: string,
unit: string,
round: number
}> = {
HP: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.HPAdd,
avatarData.level
),
name: transI18n("hp"),
icon: "spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "",
round: 0
},
ATK: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.AttackAdd,
avatarData.level
),
name: transI18n("atk"),
icon: "spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "",
round: 0
},
DEF: {
value: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceBase,
mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.DefenceAdd,
avatarData.level
),
name: transI18n("def"),
icon: "spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "",
round: 0
},
SPD: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.SpeedBase || 0,
name: transI18n("speed"),
icon: "spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
round: 1
},
CRITRate: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalChance || 0,
name: transI18n("critRate"),
icon: "spriteoutput/ui/avatar/icon/IconCriticalChance.png",
unit: "%",
round: 1
},
CRITDmg: {
value: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
base: mapAvatar?.[avatarSelected?.ID?.toString()]?.Stats[charPromotion]?.CriticalDamage || 0,
name: transI18n("critDmg"),
icon: "spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
unit: "%",
round: 1
},
BreakEffect: {
value: 0,
base: 0,
name: transI18n("breakEffect"),
icon: "spriteoutput/ui/avatar/icon/IconBreakUp.png",
unit: "%",
round: 1
},
EffectRES: {
value: 0,
base: 0,
name: transI18n("effectRes"),
icon: "spriteoutput/ui/avatar/icon/IconStatusResistance.png",
unit: "%",
round: 1
},
EnergyRate: {
value: 0,
base: 0,
name: transI18n("energyRegenerationRate"),
icon: "spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
unit: "%",
round: 1
},
EffectHitRate: {
value: 0,
base: 0,
name: transI18n("effectHitRate"),
icon: "spriteoutput/ui/avatar/icon/IconStatusProbability.png",
unit: "%",
round: 1
},
HealBoost: {
value: 0,
base: 0,
name: transI18n("outgoingHealingBoost"),
icon: "spriteoutput/ui/avatar/icon/IconHealRatio.png",
unit: "%",
round: 1
},
PhysicalAdd: {
value: 0,
base: 0,
name: transI18n("physicalDmgBoost"),
icon: "spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
unit: "%",
round: 1
},
FireAdd: {
value: 0,
base: 0,
name: transI18n("fireDmgBoost"),
icon: "spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
unit: "%",
round: 1
},
IceAdd: {
value: 0,
base: 0,
name: transI18n("iceDmgBoost"),
icon: "spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
unit: "%",
round: 1
},
ThunderAdd: {
value: 0,
base: 0,
name: transI18n("thunderDmgBoost"),
icon: "spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
unit: "%",
round: 1
},
WindAdd: {
value: 0,
base: 0,
name: transI18n("windDmgBoost"),
icon: "spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
unit: "%",
round: 1
},
QuantumAdd: {
value: 0,
base: 0,
name: transI18n("quantumDmgBoost"),
icon: "spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
unit: "%",
round: 1
},
ImaginaryAdd: {
value: 0,
base: 0,
name: transI18n("imaginaryDmgBoost"),
icon: "spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
unit: "%",
round: 1
},
ElationAdd: {
value: 0,
base: 0,
name: transI18n("elation"),
icon: "spriteoutput/ui/avatar/icon/IconJoy.png",
unit: "%",
round: 1
}
}
if (avatarProfile?.lightcone && mapLightCone[avatarProfile?.lightcone?.item_id]) {
const lightconePromotion = calcPromotion(avatarProfile?.lightcone?.level)
statsData.HP.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd,
avatarProfile?.lightcone?.level
)
statsData.HP.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseHP,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd,
avatarProfile?.lightcone?.level
)
statsData.ATK.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
avatarProfile?.lightcone?.level
)
statsData.ATK.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttack,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseAttackAdd,
avatarProfile?.lightcone?.level
)
statsData.DEF.value += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
avatarProfile?.lightcone?.level
)
statsData.DEF.base += calcBaseStatRaw(
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefence,
mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Stats?.[lightconePromotion]?.BaseDefenceAdd,
avatarProfile?.lightcone?.level
)
const bonusData = mapLightCone?.[avatarProfile?.lightcone?.item_id]?.Skills?.Level?.[avatarProfile?.lightcone.rank]?.Bonus
if (bonusData && bonusData.length > 0) {
const bonusSpd = bonusData.filter((bonus) => bonus.PropertyType === "BaseSpeed")
const bonusOther = bonusData.filter((bonus) => bonus.PropertyType !== "BaseSpeed")
bonusSpd.forEach((bonus) => {
statsData.SPD.value += bonus.Value
statsData.SPD.base += bonus.Value
})
bonusOther.forEach((bonus) => {
const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
}
})
}
}
if (avatarSkillTree) {
Object.values(avatarSkillTree).forEach((value) => {
if (value?.["1"]
&& value?.["1"]?.PointID
&& typeof avatarData?.data?.skills?.[value?.["1"]?.PointID] === "number"
&& avatarData?.data?.skills?.[value?.["1"]?.PointID] !== 0
&& value?.["1"]?.StatusAddList
&& value?.["1"].StatusAddList.length > 0) {
value?.["1"]?.StatusAddList.forEach((status) => {
const statsBase = mappingStats?.[status?.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(status?.PropertyType, statsData[statsBase].base, status.Value)
}
})
}
})
}
if (avatarProfile?.relics && mainAffix && subAffix) {
Object.entries(avatarProfile?.relics).forEach(([key, value]) => {
const mainAffixMap = mainAffix["5" + key]
const subAffixMap = subAffix["5"]
if (!mainAffixMap || !subAffixMap) return
const mainStats = mappingStats?.[mainAffixMap?.[value.main_affix_id]?.Property]?.baseStat
if (mainStats && statsData[mainStats]) {
statsData[mainStats].value += calcMainAffixBonusRaw(mainAffixMap?.[value.main_affix_id], value.level, statsData[mainStats].base)
}
value?.sub_affixes.forEach((subValue) => {
const subStats = mappingStats?.[subAffixMap?.[subValue.sub_affix_id]?.Property]?.baseStat
if (subStats && statsData[subStats]) {
statsData[subStats].value += calcSubAffixBonusRaw(subAffixMap?.[subValue.sub_affix_id], subValue.step, subValue.count, statsData[subStats].base)
}
})
})
}
if (relicEffects && relicEffects.length > 0) {
relicEffects.forEach((relic) => {
const dataBonus = mapRelicSet?.[relic.key]?.Skills
if (!dataBonus || Object.keys(dataBonus).length === 0) return
Object.entries(dataBonus || {}).forEach(([key, value]) => {
if (relic.count < Number(key)) return
value.Bonus.forEach((bonus) => {
const statsBase = mappingStats?.[bonus.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(bonus.PropertyType, statsData[statsBase].base, bonus.Value)
}
})
})
})
}
return statsData
}, [
avatarSelected,
avatarData,
mapAvatar,
avatarProfile?.lightcone,
avatarProfile?.relics,
mapLightCone,
mainAffix,
subAffix,
relicEffects,
mapRelicSet,
avatarSkillTree
])
return (
<div className="grid grid-cols-1 lg:grid-cols-12 gap-8 pt-1">
{/* Stats Overview Column - Clean Minimal Grid, No Box-in-Box */}
<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) => {
if (!stat || (key.includes("Add") && stat.value === 0)) return null
return (
<div
key={index}
className="flex items-center justify-between py-1.5 border-b border-base-content/5"
>
<div className="flex items-center gap-2 min-w-0 pr-2">
<NextImage
src={`${process.env.CDN_URL}/${stat?.icon}`}
unoptimized
crossOrigin="anonymous"
alt={stat.name}
width={20}
height={20}
className="h-5 w-5 theme-stat-icon shrink-0 object-contain"
/>
<span className="font-medium text-base-content/80 truncate">{stat.name}</span>
</div>
<span className="font-mono font-semibold text-primary shrink-0 text-right">
{stat.value ? stat.unit === "%" ? (stat.value * 100).toFixed(stat.round) : stat.value.toFixed(stat.round) : 0}
<span className="text-[10px] font-normal opacity-80 ml-0.5">{stat.unit}</span>
</span>
</div>
)
})}
</div>
</div>
{/* 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) => {
const relicInfo = mapRelicSet[setEffect.key];
if (!relicInfo) return null;
return (
<div
key={index}
className="flex items-center justify-between text-xs py-1.5 border-b border-base-content/5"
>
<div
className="font-medium text-base-content/90 truncate max-w-[80%]"
dangerouslySetInnerHTML={{
__html: replaceByParam(getLocaleName(locale, relicInfo.Name), [])
}}
/>
<span className="font-mono font-semibold text-amber-600 dark:text-amber-400 text-xs shrink-0">
{setEffect.count} Pc
</span>
</div>
)
})}
</div>
</div>
)}
</div>
{/* Relic Equipment Cards Column */}
<div className="lg:col-span-6 space-y-2">
{relicStats?.map((relic, index) => {
if (!relic || !avatarSelected) return null
return (
<RelicShowcase key={index} relic={relic} avatarInfo={avatarSelected} />
)
})}
{(!relicStats || !relicStats?.length) && (
<div className="p-8 text-center text-base-content/40 text-xs italic">
{transI18n("noRelicEquipped")}
</div>
)}
</div>
</div>
)
}
File diff suppressed because it is too large Load Diff
+434 -366
View File
@@ -1,366 +1,434 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import { useCallback, useEffect, useMemo } from "react";
import RelicMaker from "../relicBar";
import { motion } from "framer-motion";
import useUserDataStore from "@/stores/userDataStore";
import { useTranslations } from "next-intl";
import RelicCard from "../card/relicCard";
import useModelStore from '@/stores/modelStore';
import { replaceByParam } from '@/helper';
import useRelicMakerStore from '@/stores/relicMakerStore';
import QuickView from "../quickView";
import { ModalConfig } from "@/types";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { getLocaleName } from '@/helper';
import useLocaleStore from "@/stores/localeStore";
export default function RelicsInfo() {
const { avatars, setAvatars } = useUserDataStore()
const {
setSelectedRelicSlot,
selectedRelicSlot,
setSelectedMainStat,
setSelectedRelicSet,
setSelectedRelicLevel,
setListSelectedSubStats,
resetHistory,
resetSubStat,
listSelectedSubStats,
} = useRelicMakerStore()
const {
isOpenRelic,
setIsOpenRelic,
isOpenQuickView,
setIsOpenQuickView
} = useModelStore()
const transI18n = useTranslations("DataPage")
const { avatarSelected } = useCurrentDataStore()
const { mapRelicSet, subAffix } = useDetailDataStore()
const { locale } = useLocaleStore()
const handleShow = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.showModal();
}
};
// Close modal handler
const handleCloseModal = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.close();
}
};
const getRelic = useCallback((slot: string) => {
const avatar = avatars[avatarSelected?.ID.toString() || ""];
if (avatar) {
return avatar.profileList[avatar.profileSelect]?.relics[slot] || null;
}
return null;
}, [avatars, avatarSelected]);
const handlerDeleteRelic = (slot: string) => {
const avatar = avatars[avatarSelected?.ID.toString() || ""];
if (avatar) {
delete avatar.profileList[avatar.profileSelect].relics[slot]
setAvatars({ ...avatars });
}
}
const handlerChangeRelic = (slot: string) => {
const relic = getRelic(slot)
setSelectedRelicSlot(slot)
resetSubStat()
resetHistory(null)
if (relic) {
setSelectedMainStat(relic.main_affix_id.toString())
setSelectedRelicSet(relic.relic_set_id.toString())
setSelectedRelicLevel(relic.level)
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
relic.sub_affixes.forEach((item, index) => {
newSubAffixes[index].affixId = item.sub_affix_id.toString();
newSubAffixes[index].property = subAffix["5"][item.sub_affix_id.toString()]?.Property || "";
newSubAffixes[index].rollCount = item.count || 0;
newSubAffixes[index].stepCount = item.step || 0;
})
setListSelectedSubStats(newSubAffixes)
} else {
setSelectedMainStat("")
setSelectedRelicSet("")
setSelectedRelicLevel(15)
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
newSubAffixes.forEach((item) => {
item.affixId = ""
item.property = ""
item.rollCount = 0
item.stepCount = 0
})
setListSelectedSubStats(newSubAffixes)
}
setIsOpenRelic(true)
handleShow("action_detail_modal")
}
const relicEffects = useMemo(() => {
const avatar = avatars[avatarSelected?.ID.toString() || ""];
const relicCount: { [key: string]: number } = {};
if (avatar) {
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
if (relicCount[relic.relic_set_id]) {
relicCount[relic.relic_set_id]++;
} else {
relicCount[relic.relic_set_id] = 1;
}
}
}
const listEffects: { key: string, count: number }[] = [];
Object.entries(relicCount).forEach(([key, value]) => {
if (value >= 2) {
listEffects.push({ key: key, count: value });
}
});
return listEffects;
}, [avatars, avatarSelected]);
const modalConfigs: ModalConfig[] = [
{
id: "action_detail_modal",
title: "",
isOpen: isOpenRelic,
onClose: () => {
setIsOpenRelic(false)
handleCloseModal("action_detail_modal")
},
content: <RelicMaker />
},
{
id: "quick_view_modal",
title: transI18n("quickView").toUpperCase(),
isOpen: isOpenQuickView,
onClose: () => {
setIsOpenQuickView(false)
handleCloseModal("quick_view_modal")
},
content: <QuickView />
}
]
// Handle ESC key to close modal
useEffect(() => {
for (const item of modalConfigs) {
if (!item?.isOpen) {
handleCloseModal(item?.id || "")
}
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
for (const item of modalConfigs) {
handleCloseModal(item?.id || "")
}
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenRelic]);
return (
<div className="max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left Section - Items Grid */}
<div className="lg:col-span-2">
<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("relics")}
</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
{["1", "2", "3", "4", "5", "6"].map((item, index) => (
<div key={index} className="relative group">
<div
onClick={() => {
if (item === selectedRelicSlot) {
setSelectedRelicSlot("")
} else {
setSelectedRelicSlot(item)
}
handlerChangeRelic(item)
}}
className="cursor-pointer"
>
<RelicCard
slot={item}
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">
<button
onClick={(e) => {
e.stopPropagation()
handlerChangeRelic(item)
}}
className="btn btn-info p-1.5 rounded-full shadow-lg transition-colors duration-200"
title={transI18n("changeRelic")}
>
<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" />
</svg>
</button>
{getRelic(item) && (
<button
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${item}?`)) {
handlerDeleteRelic(item)
}
}}
className="btn btn-error p-1.5 rounded-full shadow-lg transition-colors duration-200"
title={transI18n("deleteRelic")}
>
<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" />
</svg>
</button>
)}
</div>
</div>
))}
</div>
<div className="grid grid-cols-2 gap-2 mt-10">
<button
disabled={!selectedRelicSlot}
onClick={() => {
handlerChangeRelic(selectedRelicSlot)
}}
className="btn btn-info"
>
{transI18n("changeRelic")}
</button>
<button
disabled={!selectedRelicSlot}
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${selectedRelicSlot}?`)) {
handlerDeleteRelic(selectedRelicSlot)
}
}}
className="btn btn-error"
>
{transI18n("deleteRelic")}
</button>
</div>
<button
onClick={() => {
setIsOpenQuickView(true)
handleShow("quick_view_modal")
}}
className="btn btn-info w-full mt-2"
>
{transI18n("quickView")}
</button>
</div>
</div>
{/* Right Section - Stats and Set Effects */}
<div className="space-y-6">
{/* Set Effects Panel */}
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h3 className="flex items-center gap-2 text-xl font-bold mb-4 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("setEffects")}
</h3>
<div className="space-y-6">
{relicEffects.map((setEffect, index) => {
const relicInfo = mapRelicSet[setEffect.key];
if (!relicInfo) return null;
return (
<div key={index} className="space-y-3">
<div className="flex items-center gap-2">
<div
className="font-bold text-warning"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, relicInfo.Name),
[]
)
}}
/>
{setEffect.count && (
<span className={`text-sm text-info`}>
({setEffect.count})
</span>
)}
</div>
<div className="space-y-2 pl-4">
{Object.entries(relicInfo.Skills).map(([requireNum, value]) => {
if (Number(requireNum) > Number(setEffect.count)) return null;
return (
<div key={requireNum} className="space-y-1">
<div className={`font-medium text-success`}>
{requireNum}-PC:
</div>
<div
className="text-sm text-base-content/80 leading-relaxed pl-4"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, value.Desc),
value.Param || []
)
}}
/>
</div>
)
})}
</div>
</div>
)
})}
</div>
</div>
</div>
</div>
{modalConfigs.map(({ id, title, onClose, content }) => (
<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="sticky top-0 z-10">
<motion.button
whileHover={{ scale: 1.1, rotate: 90 }}
transition={{ duration: 0.2 }}
className="btn btn-circle btn-md absolute right-2 top-2 bg-red-600 hover:bg-red-700 text-white border-none"
onClick={onClose}
>
✕
</motion.button>
</div>
{title && (
<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}
</div>
</dialog>
))}
</div>
);
}
/* eslint-disable react-hooks/exhaustive-deps */
"use client";
import { useCallback, useEffect, useMemo } from "react";
import RelicMaker from "../relicBar";
import useUserDataStore from "@/stores/userDataStore";
import { useTranslations } from "next-intl";
import RelicCard from "../card/relicCard";
import useModelStore from '@/stores/modelStore';
import { replaceByParam } from '@/helper';
import useRelicMakerStore from '@/stores/relicMakerStore';
import QuickView from "../quickView";
import { ModalConfig } from "@/types";
import useCurrentDataStore from "@/stores/currentDataStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { getLocaleName } from '@/helper';
import useLocaleStore from "@/stores/localeStore";
import { useShallow } from 'zustand/react/shallow';
export default function RelicsInfo() {
const { avatars, setAvatar } = useUserDataStore(
useShallow((state) => ({
avatars: state.avatars,
setAvatar: state.setAvatar,
}))
)
const {
setSelectedRelicSlot,
selectedRelicSlot,
setSelectedMainStat,
setSelectedRelicSet,
setSelectedRelicLevel,
setListSelectedSubStats,
resetHistory,
resetSubStat,
listSelectedSubStats,
} = 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 {
isOpenRelic,
setIsOpenRelic,
isOpenQuickView,
setIsOpenQuickView
} = useModelStore(
useShallow((state) => ({
isOpenRelic: state.isOpenRelic,
setIsOpenRelic: state.setIsOpenRelic,
isOpenQuickView: state.isOpenQuickView,
setIsOpenQuickView: state.setIsOpenQuickView,
}))
)
const transI18n = useTranslations("DataPage")
const { avatarSelected } = useCurrentDataStore(
useShallow((state) => ({
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 modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.showModal();
}
};
// Close modal handler
const handleCloseModal = (modalId: string) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
modal.close();
}
};
const getRelic = useCallback((slot: string) => {
const avatar = avatars[avatarSelected?.ID.toString() || ""];
if (avatar) {
const profile = avatar.profileList?.[avatar.profileSelect] ?? avatar.profileList?.[0];
return profile?.relics?.[slot] || null;
}
return null;
}, [avatars, avatarSelected]);
const handlerDeleteRelic = (slot: string) => {
const avatarKey = avatarSelected?.ID.toString() || "";
const avatar = avatars[avatarKey];
if (avatar) {
const profileIndex = avatar.profileSelect;
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 relic = getRelic(slot)
setSelectedRelicSlot(slot)
resetSubStat()
resetHistory(null)
if (relic) {
setSelectedMainStat(relic.main_affix_id.toString())
setSelectedRelicSet(relic.relic_set_id.toString())
setSelectedRelicLevel(relic.level)
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
relic.sub_affixes.forEach((item, index) => {
if (newSubAffixes[index]) {
newSubAffixes[index].affixId = item.sub_affix_id.toString();
newSubAffixes[index].property = subAffix["5"]?.[item.sub_affix_id.toString()]?.Property || "";
newSubAffixes[index].rollCount = item.count || 0;
newSubAffixes[index].stepCount = item.step || 0;
}
})
setListSelectedSubStats(newSubAffixes)
} else {
setSelectedMainStat("")
setSelectedRelicSet("")
setSelectedRelicLevel(15)
const newSubAffixes: { affixId: string, property: string, rollCount: number, stepCount: number }[] = [...listSelectedSubStats];
newSubAffixes.forEach((item) => {
item.affixId = ""
item.property = ""
item.rollCount = 0
item.stepCount = 0
})
setListSelectedSubStats(newSubAffixes)
}
setIsOpenRelic(true)
handleShow("action_detail_modal")
}
const relicEffects = useMemo(() => {
const avatar = avatars[avatarSelected?.ID.toString() || ""];
const relicCount: { [key: string]: number } = {};
const currentProfile = avatar?.profileList?.[avatar?.profileSelect] ?? avatar?.profileList?.[0];
if (currentProfile?.relics) {
for (const relic of Object.values(currentProfile.relics)) {
if (relic) {
relicCount[relic.relic_set_id] = (relicCount[relic.relic_set_id] || 0) + 1;
}
}
}
const listEffects: { key: string, count: number }[] = [];
Object.entries(relicCount).forEach(([key, value]) => {
if (value >= 2) {
listEffects.push({ key: key, count: value });
}
});
return listEffects;
}, [avatars, avatarSelected]);
const modalConfigs: ModalConfig[] = [
{
id: "action_detail_modal",
title: transI18n("changeRelic") || "Cài đặt di vật",
isOpen: isOpenRelic,
onClose: () => {
setIsOpenRelic(false)
handleCloseModal("action_detail_modal")
},
content: <RelicMaker />
},
{
id: "quick_view_modal",
title: transI18n("quickView").toUpperCase(),
isOpen: isOpenQuickView,
onClose: () => {
setIsOpenQuickView(false)
handleCloseModal("quick_view_modal")
},
content: <QuickView />
}
]
// Handle ESC key to close modal
useEffect(() => {
for (const item of modalConfigs) {
if (!item?.isOpen) {
handleCloseModal(item?.id || "")
}
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
for (const item of modalConfigs) {
handleCloseModal(item?.id || "")
}
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenRelic]);
return (
<div className="w-full space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6">
{/* Left Section - Relic Equipment Grid */}
<div className="lg:col-span-2 space-y-4">
<div className="glass-card rounded-2xl p-4 sm:p-6 border border-base-content/10 shadow-xs space-y-6">
<div className="flex items-center justify-between pb-3 border-b border-base-content/10">
<h2 className="text-lg sm:text-xl font-bold flex items-center gap-2.5">
<div className="w-1.5 h-5 bg-linear-to-b from-primary to-accent rounded-full"></div>
<span className="bg-clip-text text-transparent bg-linear-to-r from-primary to-accent font-bold tracking-tight">
{transI18n("relics")}
</span>
</h2>
<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) => (
<div
key={index}
onClick={() => {
if (item === selectedRelicSlot) {
setSelectedRelicSlot("")
} else {
setSelectedRelicSlot(item)
}
handlerChangeRelic(item)
}}
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
slot={item}
avatarId={avatarSelected?.ID.toString() || ""}
/>
{/* Micro Actions */}
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
<button
onClick={(e) => {
e.stopPropagation()
handlerChangeRelic(item)
}}
className="btn btn-xs btn-circle btn-primary shadow-xs"
title={transI18n("changeRelic")}
>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536M4 20h4l10.293-10.293a1 1 0 000-1.414l-2.586-2.586a1 1 0 00-1.414 0L4 16v4z" />
</svg>
</button>
{getRelic(item) && (
<button
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${item}?`)) {
handlerDeleteRelic(item)
}
}}
className="btn btn-xs btn-circle btn-error shadow-xs"
title={transI18n("deleteRelic")}
>
<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-6v6M9 7V4h6v3m5 0H4" />
</svg>
</button>
)}
</div>
</div>
))}
</div>
{/* Bottom Actions Bar */}
<div className="grid grid-cols-2 gap-3 pt-2">
<button
disabled={!selectedRelicSlot}
onClick={() => handlerChangeRelic(selectedRelicSlot)}
className="btn btn-primary btn-sm rounded-xl font-bold shadow-xs"
>
{transI18n("changeRelic")}
</button>
<button
disabled={!selectedRelicSlot}
onClick={(e) => {
e.stopPropagation()
if (window.confirm(`${transI18n("deleteRelicConfirm")} ${selectedRelicSlot}?`)) {
handlerDeleteRelic(selectedRelicSlot)
}
}}
className="btn btn-ghost btn-sm text-error hover:bg-error/20 rounded-xl"
>
{transI18n("deleteRelic")}
</button>
</div>
</div>
</div>
{/* Right Section - Set Effects Showcase */}
<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">
<div className="pb-3 border-b border-base-content/10">
<h3 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-amber-400 to-amber-600 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")}
</span>
</h3>
</div>
{relicEffects.length > 0 ? (
<div className="divide-y divide-base-content/10">
{relicEffects.map((setEffect, index) => {
const relicInfo = mapRelicSet[setEffect.key];
if (!relicInfo) return null;
return (
<div key={index} className="py-3.5 first:pt-0 last:pb-0 space-y-2">
<div className="flex items-center justify-between">
<div
className="font-bold text-sm text-amber-500"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, relicInfo.Name),
[]
)
}}
/>
{setEffect.count && (
<span className="badge badge-primary badge-sm font-mono font-bold">
{setEffect.count} Pc
</span>
)}
</div>
<div className="space-y-2 pt-1">
{Object.entries(relicInfo.Skills).map(([requireNum, value]) => {
if (Number(requireNum) > Number(setEffect.count)) return null;
return (
<div key={requireNum} className="text-xs space-y-0.5 pl-2 border-l-2 border-primary/40">
<div className="font-bold text-emerald-500">
{requireNum}-PC Bonus
</div>
<div
className="text-base-content/80 leading-relaxed"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, value.Desc),
value.Param || []
)
}}
/>
</div>
)
})}
</div>
</div>
)
})}
</div>
) : (
<div className="text-center py-10 text-base-content/40 text-xs italic">
{transI18n("noRelicEquipped")}
</div>
)}
</div>
</div>
</div>
{/* Modals */}
{modalConfigs.map(({ id, title, onClose, content }) => (
<dialog key={id} id={id} className="modal">
<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-20 flex items-center justify-between px-6 py-4 border-b border-base-content/10 bg-base-100">
<h3 className="font-bold text-xl tracking-tight bg-clip-text text-transparent bg-linear-to-r from-primary via-accent to-secondary">
{title}
</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}
>
✕
</button>
</div>
<div className="p-4 sm:p-6 overflow-y-auto max-h-[calc(90vh-5rem)]">
{content}
</div>
</div>
<form method="dialog" className="modal-backdrop">
<button onClick={onClose}>close</button>
</form>
</dialog>
))}
</div>
);
}
+136 -112
View File
@@ -1,112 +1,136 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import dynamic from "next/dynamic"
import type { SingleValue } from "react-select"
import Image from "next/image"
import useLocaleStore from "@/stores/localeStore"
import ParseText from "../parseText"
import { themeColors } from "@/constant/constant"
import type { Props as SelectProps } from "react-select"
import { JSX } from "react"
const Select = dynamic(
() => import("react-select").then(m => m.default),
{ ssr: false }
) as <Option, IsMulti extends boolean = false>(
props: SelectProps<Option, IsMulti>
) => JSX.Element
export type SelectOption = {
value: string
label: string
imageUrl: string
}
type SelectCustomProp = {
customSet: SelectOption[]
excludeSet: SelectOption[]
selectedCustomSet: string
placeholder: string
setSelectedCustomSet: (value: string) => void
}
export default function SelectCustomImage({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const options: SelectOption[] = customSet
const { locale, theme } = useLocaleStore()
const c = themeColors[theme] || themeColors.winter
const customStyles = {
option: (p: any, s: any) => ({
...p,
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '8px 12px',
backgroundColor: s.isFocused ? c.bgHover : c.bg,
color: c.text,
cursor: 'pointer'
}),
singleValue: (p: any) => ({
...p,
display: 'flex',
alignItems: 'center',
gap: '8px',
color: c.text
}),
control: (p: any) => ({
...p,
backgroundColor: c.bg,
borderColor: c.border,
boxShadow: 'none'
}),
menu: (p: any) => ({
...p,
backgroundColor: c.bg,
color: c.text,
zIndex: 9999
}),
menuPortal: (p: any) => ({
...p,
zIndex: 9999
})
}
const formatOptionLabel = (option: SelectOption) => (
<div className="flex items-center gap-1 w-full h-full">
<Image
unoptimized
crossOrigin="anonymous"
src={option.imageUrl}
alt=""
width={125}
height={125}
className="w-8 h-8 object-contain bg-warning-content rounded-full"
/>
<ParseText className='font-bold' text={option.label} locale={locale} />
</div>
)
return (
<Select
options={options.filter(opt => !excludeSet.some(ex => ex.value === opt.value))}
value={options.find(opt => {
return opt.value === selectedCustomSet
}) || null}
onChange={(selected: SingleValue<SelectOption>) => {
setSelectedCustomSet(selected?.value || '')
}}
formatOptionLabel={formatOptionLabel}
styles={customStyles}
placeholder={placeholder}
className="my-react-select-container"
classNamePrefix="my-react-select"
isSearchable
isClearable
/>
)
}
/* eslint-disable @typescript-eslint/no-explicit-any */
'use client'
import dynamic from "next/dynamic"
import type { SingleValue } from "react-select"
import Image from "next/image"
import useLocaleStore from "@/stores/localeStore"
import { useShallow } from "zustand/react/shallow"
import ParseText from "../parseText"
import { themeColors } from "@/constant/constant"
import type { Props as SelectProps } from "react-select"
import { JSX } from "react"
const Select = dynamic(
() => import("react-select").then(m => m.default),
{ ssr: false }
) as <Option, IsMulti extends boolean = false>(
props: SelectProps<Option, IsMulti>
) => JSX.Element
export type SelectOption = {
value: string
label: string
imageUrl: string
}
type SelectCustomProp = {
customSet: SelectOption[]
excludeSet: SelectOption[]
selectedCustomSet: string
placeholder: string
setSelectedCustomSet: (value: string) => void
}
export default function SelectCustomImage({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const options: SelectOption[] = customSet
const { locale, theme } = useLocaleStore(
useShallow((state) => ({
locale: state.locale,
theme: state.theme,
}))
)
const c = themeColors[theme] || themeColors.winter
const customStyles = {
option: (p: any, s: any) => ({
...p,
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '10px 14px',
backgroundColor: s.isFocused ? c.bgHover : c.bg,
color: c.text,
cursor: 'pointer',
opacity: 1,
}),
singleValue: (p: any) => ({
...p,
display: 'flex',
alignItems: 'center',
gap: '8px',
color: c.text,
opacity: 1,
}),
control: (p: any) => ({
...p,
backgroundColor: c.bg,
borderColor: c.border,
boxShadow: 'none',
opacity: 1,
minHeight: '38px',
borderRadius: '12px',
}),
menu: (p: any) => ({
...p,
backgroundColor: c.bg,
color: c.text,
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) => ({
...p,
zIndex: 99999,
opacity: 1,
})
}
const formatOptionLabel = (option: SelectOption) => (
<div className="flex items-center gap-1 w-full h-full">
<Image
unoptimized
crossOrigin="anonymous"
src={option.imageUrl}
alt=""
width={125}
height={125}
className="w-8 h-8 object-contain bg-warning-content rounded-full"
/>
<ParseText className='font-bold' text={option.label} locale={locale} />
</div>
)
return (
<Select
options={options.filter(opt => !excludeSet.some(ex => ex.value === opt.value))}
value={options.find(opt => {
return opt.value === selectedCustomSet
}) || null}
onChange={(selected: SingleValue<SelectOption>) => {
setSelectedCustomSet(selected?.value || '')
}}
formatOptionLabel={formatOptionLabel}
styles={customStyles}
placeholder={placeholder}
className="my-react-select-container"
classNamePrefix="my-react-select"
isSearchable
isClearable
/>
)
}
+7 -2
View File
@@ -7,6 +7,7 @@ import useLocaleStore from '@/stores/localeStore'
import { themeColors } from '@/constant/constant'
import type { Props as SelectProps } from "react-select"
import { JSX } from "react"
import { useShallow } from "zustand/react/shallow"
const Select = dynamic(
() => import("react-select").then(m => m.default),
@@ -32,7 +33,9 @@ type SelectCustomProp = {
export default function SelectCustomText({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
const options: SelectOption[] = customSet
const { theme } = useLocaleStore()
const { theme } = useLocaleStore(
useShallow((state) => ({ theme: state.theme }))
)
const c = themeColors[theme] || themeColors.winter
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
className="text-base"
dangerouslySetInnerHTML={{
File diff suppressed because it is too large Load Diff
+93 -107
View File
@@ -1,108 +1,94 @@
"use client"
import NextImage from "next/image"
import { AvatarDetail, RelicShowcaseType } from "@/types";
export default function RelicShowcase({
relic,
avatarInfo,
}: {
relic: RelicShowcaseType;
avatarInfo: AvatarDetail;
}) {
return (
<>
<div
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"
>
{/* 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
src={relic?.img || ""}
unoptimized
crossOrigin="anonymous"
width={78}
height={78}
alt="Relic Icon"
className="h-19.5 w-19.5 rounded-lg"
/>
<div
className="absolute text-yellow-400 font-bold z-10 drop-shadow-[0_0_6px_rgba(251,191,36,0.8)]"
style={{
left: '0.65rem',
bottom: '-0.45rem',
fontSize: '1.05rem',
letterSpacing: '-0.1em',
}}
>
✦✦✦✦✦
</div>
</div>
<div className=" flex w-1/6 flex-col items-center justify-center">
<div className="relative">
<div className="absolute inset-0 bg-yellow-500/15 rounded-full blur-md -z-10"></div>
<NextImage
src={`${process.env.CDN_URL}/${relic?.mainAffix?.detail?.icon}` || ""}
unoptimized
crossOrigin="anonymous"
width={35}
height={35}
alt="Main Affix Icon"
className="h-8.75 w-8.75"
/>
</div>
<span className="text-base text-yellow-400 font-semibold drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]">
{relic?.mainAffix?.valueAffix + relic?.mainAffix?.detail?.unit}
</span>
<span className="bg-black/20 backdrop-blur-sm rounded px-1.5 py-0.5 text-xs border border-white/10">
+{relic?.mainAffix?.level}
</span>
</div>
<div style={{ opacity: 0.3, height: '78px', borderLeftWidth: '1px' }}></div>
<div className="grid w-[65%] m-2 grid-cols-2 gap-1">
{relic?.subAffix?.map((subAffix, index) => {
if (!subAffix) return null
return (
<div key={index} className="flex flex-col">
<div className="relative flex flex-row items-center bg-black/20 backdrop-blur-sm rounded-md p-1 border border-white/5 min-w-0">
{subAffix?.detail?.icon ? (
<NextImage
src={`${process.env.CDN_URL}/${subAffix?.detail?.icon}` || ""}
unoptimized
crossOrigin="anonymous"
width={32}
height={32}
alt="Sub Affix Icon"
className="h-6 w-6 shrink-0"
/>
) : (
<div className="h-6 w-6 bg-black/60 rounded flex items-center justify-center border border-white/10 shrink-0">
<span className="text-xs text-white/50">?</span>
</div>
)}
<span className="text-xs text-gray-200 ml-0.5 truncate flex-1 min-w-0">
+{subAffix?.valueAffix + subAffix?.detail?.unit}
</span>
{
(avatarInfo?.Relics?.SubAffixPropertyList.findIndex((item) => item === subAffix?.property) !== -1) && (
<span className="ml-1 bg-yellow-600/20 text-yellow-400 rounded-full px-1 py-0.5 text-[10px] font-semibold border border-yellow-600/30 shrink-0 leading-none">
{subAffix?.count}
</span>
)}
</div>
</div>
)
})}
</div>
</div>
</>
)
"use client"
import NextImage from "next/image"
import { AvatarDetail, RelicShowcaseType } from "@/types";
export default function RelicShowcase({
relic,
avatarInfo,
}: {
relic: RelicShowcaseType;
avatarInfo: AvatarDetail;
}) {
return (
<div className="flex items-center gap-3 py-2 px-1 border-b border-base-content/10 last:border-b-0">
{/* Relic Figure Icon */}
<div className="relative shrink-0 flex flex-col items-center">
<NextImage
src={relic?.img || ""}
unoptimized
crossOrigin="anonymous"
width={48}
height={48}
alt="Relic"
className="h-12 w-12 object-contain"
/>
</div>
{/* Main Affix - Clean Display */}
<div className="flex w-20 shrink-0 flex-col items-start justify-center">
<div className="flex items-center gap-1">
{relic?.mainAffix?.detail?.icon ? (
<NextImage
src={`${process.env.CDN_URL}/${relic.mainAffix.detail.icon}`}
unoptimized
crossOrigin="anonymous"
width={16}
height={16}
alt="Main Affix"
className="h-4 w-4 theme-stat-icon shrink-0 object-contain"
/>
) : null}
<span className="text-xs font-bold text-amber-600 dark:text-amber-400 font-mono">
{relic?.mainAffix?.valueAffix + (relic?.mainAffix?.detail?.unit || "")}
</span>
</div>
<span className="text-[10px] font-mono text-base-content/50 ml-0.5">
+{relic?.mainAffix?.level}
</span>
</div>
{/* Sub Affixes Grid - Clean Flat Typography */}
<div className="grid flex-1 grid-cols-2 gap-x-3 gap-y-1">
{relic?.subAffix?.map((subAffix, index) => {
if (!subAffix) return null
const isOptimalStat = avatarInfo?.Relics?.SubAffixPropertyList?.includes(subAffix?.property)
return (
<div
key={index}
className={`flex items-center justify-between py-0.5 min-w-0 ${
isOptimalStat
? "text-amber-600 dark:text-amber-400 font-semibold"
: "text-base-content/75"
}`}
>
<div className="flex items-center gap-1 min-w-0 flex-1">
{subAffix?.detail?.icon ? (
<NextImage
src={`${process.env.CDN_URL}/${subAffix.detail.icon}`}
unoptimized
crossOrigin="anonymous"
width={14}
height={14}
alt="Sub Affix"
className="h-3.5 w-3.5 theme-stat-icon shrink-0 object-contain"
/>
) : null}
<span className="text-xs font-mono truncate">
+{subAffix?.valueAffix + (subAffix?.detail?.unit || "")}
</span>
</div>
{isOptimalStat && subAffix?.count ? (
<span className="ml-1 text-[10px] font-bold text-amber-600/80 dark:text-amber-400/80 shrink-0 font-mono">
({subAffix?.count})
</span>
) : null}
</div>
)
})}
</div>
</div>
)
}
+402 -384
View File
@@ -1,385 +1,403 @@
"use client"
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { traceButtonsInfo, traceLink } from "@/constant/traceConstant";
import useUserDataStore from "@/stores/userDataStore";
import useLocaleStore from "@/stores/localeStore";
import Image from "next/image";
import { replaceByParam, getLocaleName } from '@/helper';
import { mappingStats } from "@/constant/constant";
import { toast } from "react-toastify";
import useCurrentDataStore from "@/stores/currentDataStore";
import { StatusAdd } from '@/types/avatarDetail';
import { SkillDescription } from "./skillDescription";
export default function SkillsInfo() {
const transI18n = useTranslations("DataPage")
const { theme } = useLocaleStore()
const { avatarSelected, skillIDSelected, setSkillIDSelected } = useCurrentDataStore()
const { avatars, setAvatar } = useUserDataStore()
const { locale } = useLocaleStore()
const traceButtons = useMemo(() => {
if (!avatarSelected) return
return traceButtonsInfo[avatarSelected.BaseType]
}, [avatarSelected])
const avatarData = useMemo(() => {
if (!avatarSelected) return
return avatars[avatarSelected?.ID?.toString()]
}, [avatarSelected, avatars])
const avatarSkillTree = useMemo(() => {
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
if (avatars[avatarSelected?.ID?.toString()].enhanced) {
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
}
return avatarSelected?.SkillTrees || {}
}, [avatarSelected, avatars])
const skillInfo = useMemo(() => {
if (!avatarSelected || !skillIDSelected) return
return avatarSkillTree?.[skillIDSelected || ""]?.["1"]
}, [avatarSelected, avatarSkillTree, skillIDSelected])
const getTraceBuffDisplay = (status: StatusAdd) => {
const dataDisplay = mappingStats[status.PropertyType]
if (!dataDisplay) return ""
if (dataDisplay.unit === "%") {
return `${(status.Value * 100).toFixed(1)}${dataDisplay.unit}`
}
if (dataDisplay.name === "SPD") {
return `${status.Value.toFixed(1)}${dataDisplay.unit}`
}
return `${status.Value.toFixed(0)}${dataDisplay.unit}`
}
const dataLevelUpSkill = useMemo(() => {
const skillIds: number[] = skillInfo?.LevelUpSkillID || []
if (!avatarSelected || !avatarData) return undefined
let result = Object.values(avatarSelected.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
if (avatarData.enhanced) {
result = Object.values(avatarSelected?.Enhanced?.[avatarData.enhanced.toString()]?.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
}
if (result && result.length > 0) {
return {
isServant: false,
data: result,
servantData: null,
}
}
const resultServant = Object.values(avatarSelected?.Memosprite?.Skills || {})
?.filter((skill) => skillIds.includes(skill.ID))
if (resultServant && resultServant.length > 0) {
return {
isServant: true,
data: resultServant,
servantData: avatarSelected.Memosprite,
}
}
return undefined
}, [skillInfo?.LevelUpSkillID, avatarSelected, avatarData])
const handlerMaxAll = () => {
if (!avatarData || !avatarSkillTree) {
toast.error(transI18n("maxAllFailed"))
return
}
const newData = structuredClone(avatarData)
newData.data.skills = Object.values(avatarSkillTree).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>)
toast.success(transI18n("maxAllSuccess"))
setAvatar(newData)
}
const handlerChangeStatusTrace = (status: boolean) => {
if (!avatarData || !skillInfo) return
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = status ? 1 : 0
if (!status && traceLink?.[avatarSelected?.BaseType || ""]?.[skillIDSelected || ""]) {
traceLink[avatarSelected?.BaseType || ""][skillIDSelected || ""].forEach((pointId) => {
if (avatarSkillTree?.[pointId]?.["1"]) {
newData.data.skills[avatarSkillTree?.[pointId]?.["1"].PointID] = 0
}
})
}
setAvatar(newData)
}
return (
<div className="w-full">
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="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("skills")}
</h2>
<div className="flex flex-col items-center">
<button className="btn btn-success" onClick={handlerMaxAll}>{transI18n("maxAll")}</button>
{traceButtons && avatarSelected && (
<div className="grid col-span-4 relative w-full aspect-square">
<Image
unoptimized
crossOrigin="anonymous"
src={`/skilltree/${avatarSelected?.BaseType?.toUpperCase()}.webp`}
alt=""
width={312}
priority={true}
height={312}
style={{
filter: (theme === "winter" || theme === "cupcake") ? "invert(1)" : "none"
}}
className={`w-full h-full object-cover rounded-xl`}
/>
{traceButtons.map((btn, index) => {
if (!avatarSelected?.SkillTrees?.[btn.id]) {
return null
}
return (
<div
key={`${btn.id} + ${index}`}
id={btn.id}
className={`
absolute rounded-full border border-black
bg-no-repeat bg-contain
cursor-pointer transition-all duration-200 ease-in-out
shadow-[0_0_5px_white] flex justify-center items-center
hover:scale-110z-10
${btn.size === "small" ? "w-[6vw] h-[6vw] md:w-[2vw] md:h-[2vw] bg-white" : ""}
${btn.size === "medium" ? "w-[8vw] h-[8vw] md:w-[3vw] md:h-[3vw] bg-white" : ""}
${btn.size === "big" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${btn.size === "special" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-white" : ""}
${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={() => {
setSkillIDSelected(btn.id === skillIDSelected ? null : btn.id)
}}
style={{
left: btn.left,
top: btn.top,
transform: "translate(-50%, -50%)",
}}
>
<Image
src={`${process.env.CDN_URL}/${avatarSelected?.SkillTrees?.[btn.id]?.["1"]?.Icon}`}
alt={btn.id.replaceAll("Point", "")}
priority={true}
unoptimized
crossOrigin="anonymous"
width={124}
height={124}
style={{
objectFit: "contain",
padding: "2px",
filter: (btn.size !== "big" && btn.size !== "memory" && btn.size !== "elation") ? "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>
)}
{btn.size === "special" && (
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: "#4a6eff",
mixBlendMode: "screen",
opacity: 0.8,
borderRadius: "50%"
}}
/>
)}
{btn.size === "memory" && (
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: "#9a89ff",
mixBlendMode: "screen",
opacity: 0.4,
borderRadius: "50%"
}}
/>
)}
{btn.size === "elation" && (
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: "#ff8c00",
mixBlendMode: "screen",
opacity: 0.5,
borderRadius: "50%"
}}
/>
)}
{btn.size === "big" && (
<div
style={{
position: "absolute",
inset: 0,
backgroundColor: "#f5e4b0",
mixBlendMode: "screen",
opacity: 0.3,
borderRadius: "50%"
}}
/>
)}
</div>
)
})}
</div>
)}
{!traceButtons && avatarSelected && (
<div className="flex flex-col relative w-full aspect-square">
</div>
)}
</div>
</div>
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-linear-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("details")}
</h2>
{skillIDSelected && avatarSelected?.SkillTrees && avatarData && (
<div>
{skillInfo?.MaxLevel && skillInfo?.MaxLevel > 1 ? (
<div>
<div className="font-bold text-success">{transI18n("level")}</div>
<div className="w-full max-w-xs">
<input type="range"
min={1}
max={skillInfo?.MaxLevel || 1}
value={avatarData?.data.skills?.[skillInfo?.PointID] || 1}
onChange={(e) => {
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = parseInt(e.target.value)
setAvatar(newData)
}}
className="range range-success"
step="1" />
<div className="flex justify-between px-2.5 mt-2 text-xs">
{Array.from({ length: skillInfo?.MaxLevel }, (_, index) => index + 1).map((index) => (
<span key={index}>{index}</span>
))}
</div>
</div>
</div>
) : skillInfo?.MaxLevel && skillInfo?.MaxLevel === 1 && traceButtons?.find((btn) => btn.id === skillIDSelected)?.size !== "big" ? (
<div className="flex items-center gap-2">
<input
type="checkbox"
checked={avatarData?.data.skills?.[skillInfo?.PointID] === 1}
className="toggle toggle-success"
onChange={(e) => {
if (traceButtons?.find((btn) => btn.id === skillIDSelected)?.size === "special") {
if (e.target.checked) {
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = 1
setAvatar(newData)
return
}
const newData = structuredClone(avatarData)
delete newData.data.skills[skillInfo?.PointID]
setAvatar(newData)
return
}
handlerChangeStatusTrace(e.target.checked)
}}
/>
<div className="font-bold text-success">
{avatarData?.data.skills?.[skillInfo?.PointID] === 1 ? transI18n("active") : transI18n("inactive")}
</div>
</div>
) : (
null
)}
{((skillInfo?.PointName && skillInfo?.PointDesc) ||
(skillInfo?.PointName && skillInfo?.StatusAddList.length > 0))
&& (
<div className="text-xl font-bold flex items-center gap-2 mt-2">
{getLocaleName(locale, skillInfo.PointName)}
{skillInfo.StatusAddList.length > 0 && (
<div>
{skillInfo.StatusAddList.map((status, index) => (
<div key={index}>
<div className="text-xl font-bold">{getTraceBuffDisplay(status)}</div>
</div>
))}
</div>
)}
</div>
)}
{skillInfo?.PointDesc && (
<div
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, skillInfo?.PointDesc) || "",
skillInfo?.Param || []
)
}}
/>
)}
{skillInfo?.LevelUpSkillID
&& skillInfo?.LevelUpSkillID.length > 0
&& dataLevelUpSkill
&& (
<div className="mt-2 flex flex-col gap-2">
{dataLevelUpSkill?.data?.map((skill, index) => (
<div key={index}>
<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
skill={skill}
locale={locale}
avatarData={avatarData}
skillInfo={skillInfo}
/>
</div>
))}
</div>
)}
</div>
)}
</div>
</div>
</div>
);
"use client"
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import { traceButtonsInfo, traceLink } from "@/constant/traceConstant";
import useUserDataStore from "@/stores/userDataStore";
import useLocaleStore from "@/stores/localeStore";
import Image from "next/image";
import { replaceByParam, getLocaleName } from '@/helper';
import { mappingStats } from "@/constant/constant";
import { toast } from "react-toastify";
import useCurrentDataStore from "@/stores/currentDataStore";
import { StatusAdd } from '@/types/avatarDetail';
import { SkillDescription } from "./skillDescription";
import { useShallow } from "zustand/react/shallow";
export default function SkillsInfo() {
const transI18n = useTranslations("DataPage")
const { theme, locale } = useLocaleStore(
useShallow((state) => ({ theme: state.theme, locale: state.locale }))
)
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(() => {
if (!avatarSelected) return
return traceButtonsInfo[avatarSelected.BaseType]
}, [avatarSelected])
const avatarData = useMemo(() => {
if (!avatarSelected) return
return avatars[avatarSelected?.ID?.toString()]
}, [avatarSelected, avatars])
const avatarSkillTree = useMemo(() => {
if (!avatarSelected || !avatars[avatarSelected?.ID?.toString()]) return {}
if (avatars[avatarSelected?.ID?.toString()].enhanced) {
return avatarSelected?.Enhanced?.[avatars[avatarSelected?.ID?.toString()].enhanced.toString()].SkillTrees || {}
}
return avatarSelected?.SkillTrees || {}
}, [avatarSelected, avatars])
const skillInfo = useMemo(() => {
if (!avatarSelected || !skillIDSelected) return
return avatarSkillTree?.[skillIDSelected || ""]?.["1"]
}, [avatarSelected, avatarSkillTree, skillIDSelected])
const getTraceBuffDisplay = (status: StatusAdd) => {
const dataDisplay = mappingStats[status.PropertyType]
if (!dataDisplay) return ""
if (dataDisplay.unit === "%") {
return `${(status.Value * 100).toFixed(1)}${dataDisplay.unit}`
}
if (dataDisplay.name === "SPD") {
return `${status.Value.toFixed(1)}${dataDisplay.unit}`
}
return `${status.Value.toFixed(0)}${dataDisplay.unit}`
}
const dataLevelUpSkill = useMemo(() => {
const skillIds: number[] = skillInfo?.LevelUpSkillID || []
if (!avatarSelected || !avatarData) return undefined
let result = Object.values(avatarSelected.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
if (avatarData.enhanced) {
result = Object.values(avatarSelected?.Enhanced?.[avatarData.enhanced.toString()]?.Skills || {})?.filter((skill) => skillIds.includes(skill.ID))
}
if (result && result.length > 0) {
return {
isServant: false,
data: result,
servantData: null,
}
}
const resultServant = Object.values(avatarSelected?.Memosprite?.Skills || {})
?.filter((skill) => skillIds.includes(skill.ID))
if (resultServant && resultServant.length > 0) {
return {
isServant: true,
data: resultServant,
servantData: avatarSelected.Memosprite,
}
}
return undefined
}, [skillInfo?.LevelUpSkillID, avatarSelected, avatarData])
const handlerMaxAll = () => {
if (!avatarData || !avatarSkillTree) {
toast.error(transI18n("maxAllFailed"))
return
}
const newData = structuredClone(avatarData)
newData.data.skills = Object.values(avatarSkillTree).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>)
toast.success(transI18n("maxAllSuccess"))
setAvatar(newData)
}
const handlerChangeStatusTrace = (status: boolean) => {
if (!avatarData || !skillInfo) return
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = status ? 1 : 0
if (!status && traceLink?.[avatarSelected?.BaseType || ""]?.[skillIDSelected || ""]) {
traceLink[avatarSelected?.BaseType || ""][skillIDSelected || ""].forEach((pointId) => {
if (avatarSkillTree?.[pointId]?.["1"]) {
newData.data.skills[avatarSkillTree?.[pointId]?.["1"].PointID] = 0
}
})
}
setAvatar(newData)
}
return (
<div className="w-full space-y-4">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 sm:gap-6">
{/* Left Panel: Skill Tree Visualizer */}
<div className="lg:col-span-6 space-y-4">
<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")}
</span>
</h2>
<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 && (
<div className="relative w-full max-w-md aspect-square">
<Image
unoptimized
crossOrigin="anonymous"
src={`/skilltree/${avatarSelected?.BaseType?.toUpperCase()}.webp`}
alt="Skill Tree"
width={312}
priority={true}
height={312}
style={{
filter: (theme === "winter" || theme === "cupcake") ? "invert(1)" : "none"
}}
className="w-full h-full object-cover"
/>
{traceButtons.map((btn, index) => {
if (!avatarSelected?.SkillTrees?.[btn.id]) {
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 (
<div
key={`${btn.id}-${index}`}
id={btn.id}
className={`
absolute rounded-full border border-black/70 cursor-pointer transition-all duration-150 ease-out flex justify-center items-center select-none overflow-visible
${btn.size === "small" ? "w-[6.5vw] h-[6.5vw] md:w-[2.2vw] md:h-[2.2vw] bg-white" : ""}
${btn.size === "medium" ? "w-[8.5vw] h-[8.5vw] md:w-[3vw] md:h-[3vw] bg-white" : ""}
${btn.size === "big" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${btn.size === "special" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-white" : ""}
${btn.size === "memory" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${btn.size === "elation" ? "w-[9.5vw] h-[9.5vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${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"}
${!isLearned ? "opacity-40 grayscale" : "shadow-[0_0_6px_rgba(255,255,255,0.4)]"}
`}
onClick={() => {
setSkillIDSelected(btn.id === skillIDSelected ? null : btn.id)
}}
style={{
left: btn.left,
top: btn.top,
transform: "translate(-50%, -50%)",
}}
>
<Image
src={`${process.env.CDN_URL}/${avatarSelected?.SkillTrees?.[btn.id]?.["1"]?.Icon}`}
alt={btn.id.replaceAll("Point", "")}
priority={true}
unoptimized
crossOrigin="anonymous"
width={124}
height={124}
style={{
objectFit: "contain",
padding: "2px",
filter: !isMajor ? "brightness(0%)" : ""
}}
/>
{/* Skill Type Color Overlays */}
{btn.size === "special" && (
<div
className="absolute inset-0 rounded-full pointer-events-none"
style={{
backgroundColor: "#4a6eff",
mixBlendMode: "screen",
opacity: 0.8,
}}
/>
)}
{btn.size === "memory" && (
<div
className="absolute inset-0 rounded-full pointer-events-none"
style={{
backgroundColor: "#9a89ff",
mixBlendMode: "screen",
opacity: 0.45,
}}
/>
)}
{btn.size === "elation" && (
<div
className="absolute inset-0 rounded-full pointer-events-none"
style={{
backgroundColor: "#ff8c00",
mixBlendMode: "screen",
opacity: 0.5,
}}
/>
)}
{btn.size === "big" && (
<div
className="absolute inset-0 rounded-full pointer-events-none"
style={{
backgroundColor: "#f5e4b0",
mixBlendMode: "screen",
opacity: 0.35,
}}
/>
)}
{isMajor && (
<p className="z-12 text-[10px] sm:text-xs font-mono font-bold text-center rounded-full absolute translate-y-full mt-0.5 bg-black/80 text-white px-1.5 py-0.2 border border-white/20 left-1/2 -translate-x-1/2 shadow-xs whitespace-nowrap">
{`${avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] || 0}/${avatarSkillTree?.[btn.id]?.["1"]?.MaxLevel}`}
</p>
)}
</div>
)
})}
</div>
)}
</div>
</div>
</div>
{/* Right Panel: Skill Details */}
<div className="lg:col-span-6 space-y-4">
<div className="glass-card rounded-2xl p-4 sm:p-6 border border-base-300 shadow-sm space-y-5">
<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")}
</span>
</h2>
</div>
{skillIDSelected && avatarSelected?.SkillTrees && avatarData ? (
<div className="space-y-4">
{/* Level Slider */}
{skillInfo?.MaxLevel && skillInfo?.MaxLevel > 1 ? (
<div className="space-y-1.5 pb-2">
<div className="text-sm font-bold text-base-content/80">
{transI18n("level")}
</div>
<div className="w-full max-w-sm space-y-1">
<input
type="range"
min={1}
max={skillInfo?.MaxLevel || 1}
value={avatarData?.data.skills?.[skillInfo?.PointID] || 1}
onChange={(e) => {
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = parseInt(e.target.value)
setAvatar(newData)
}}
className="range range-primary range-xs w-full"
step="1"
/>
<div className="flex justify-between px-1 text-[11px] font-mono text-base-content/50">
{Array.from({ length: skillInfo?.MaxLevel || 1 }, (_, index) => index + 1).map((idx) => (
<span key={idx}>{idx}</span>
))}
</div>
</div>
</div>
) : skillInfo?.MaxLevel && skillInfo?.MaxLevel === 1 && traceButtons?.find((btn) => btn.id === skillIDSelected)?.size !== "big" ? (
<div className="flex items-center gap-3 pb-2">
<span className="text-sm font-bold text-base-content/80">{transI18n("status")}</span>
<input
type="checkbox"
checked={avatarData?.data.skills?.[skillInfo?.PointID] === 1}
className="toggle toggle-primary toggle-sm"
onChange={(e) => {
if (traceButtons?.find((btn) => btn.id === skillIDSelected)?.size === "special") {
if (e.target.checked) {
const newData = structuredClone(avatarData)
newData.data.skills[skillInfo?.PointID] = 1
setAvatar(newData)
return
}
const newData = structuredClone(avatarData)
delete newData.data.skills[skillInfo?.PointID]
setAvatar(newData)
return
}
handlerChangeStatusTrace(e.target.checked)
}}
/>
<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")}
</span>
</div>
) : null}
{/* Point Name & Stat Additions */}
{((skillInfo?.PointName && skillInfo?.PointDesc) ||
(skillInfo?.PointName && skillInfo?.StatusAddList.length > 0)) && (
<div className="space-y-1.5">
<h3 className="text-lg font-extrabold text-base-content">
{getLocaleName(locale, skillInfo.PointName)}
</h3>
{skillInfo.StatusAddList.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{skillInfo.StatusAddList.map((status, 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">
+{getTraceBuffDisplay(status)}
</span>
))}
</div>
)}
</div>
)}
{/* Skill Description Prose */}
{skillInfo?.PointDesc && (
<div
className="text-xs sm:text-sm text-base-content/85 leading-relaxed"
dangerouslySetInnerHTML={{
__html: replaceByParam(
getLocaleName(locale, skillInfo?.PointDesc) || "",
skillInfo?.Param || []
)
}}
/>
)}
{/* Level-up Skill Items (Major / Servant Skills) */}
{skillInfo?.LevelUpSkillID && skillInfo?.LevelUpSkillID.length > 0 && dataLevelUpSkill && (
<div className="space-y-6 pt-1">
{dataLevelUpSkill?.data?.map((skill, index) => (
<div key={index} className={index > 0 ? "border-t border-base-content/10 pt-4" : ""}>
<SkillDescription
skill={skill}
locale={locale}
avatarData={avatarData}
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 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>
);
}
+57 -40
View File
@@ -1,41 +1,58 @@
import { getLocaleName, replaceByParam } from "@/helper";
import { AvatarStore, SkillDetail, SkillTreePoint } from "@/types";
import ExtraEffectList from "../extraInfo";
export const SkillDescription = ({ skill, locale, avatarData, skillInfo }: {
skill: SkillDetail,
locale: string,
avatarData: AvatarStore,
skillInfo: SkillTreePoint
}) => {
const levelKey = avatarData?.data.skills?.[skillInfo?.PointID]?.toString() || "";
const params = skill.Level[levelKey]?.Param || [];
const descHtml = getLocaleName(locale, skill.Desc) || getLocaleName(locale, skill.SimpleDesc);
const extraList = Object.values(skill.Extra).length > 0
? skill.Extra
: skill?.SimpleExtra || {};
return (
<div className="flex flex-col gap-2">
<div className="space-y-2 pb-2">
<div className="flex items-center gap-2">
<div className="w-1 h-5 bg-primary/80 rounded-sm" />
<div
className="text-lg font-bold tracking-wide text-foreground uppercase"
dangerouslySetInnerHTML={{ __html: replaceByParam(getLocaleName(locale, skill.Name), []) }}
/>
</div>
<div
className="text-[15px] leading-relaxed text-foreground/90 pl-3 border-l border-transparent"
dangerouslySetInnerHTML={{ __html: replaceByParam(descHtml, params) }}
/>
</div>
{Object.keys(extraList).length > 0 && (
<ExtraEffectList extras={extraList} locale={locale} />
)}
</div>
);
import { getLocaleName, replaceByParam } from "@/helper";
import { AvatarStore, SkillDetail, SkillTreePoint } from "@/types";
import ExtraEffectList from "../extraInfo";
export const SkillDescription = ({ skill, locale, avatarData, skillInfo, attackTypeLabel, skillEffectLabel }: {
skill: SkillDetail,
locale: string,
avatarData: AvatarStore,
skillInfo: SkillTreePoint,
attackTypeLabel?: string,
skillEffectLabel?: string
}) => {
const levelKey = avatarData?.data.skills?.[skillInfo?.PointID]?.toString() || "";
const params = skill.Level[levelKey]?.Param || [];
const descHtml = getLocaleName(locale, skill.Desc) || getLocaleName(locale, skill.SimpleDesc);
const extraList = Object.values(skill.Extra || {}).length > 0
? skill.Extra
: skill?.SimpleExtra || {};
const skillName = getLocaleName(locale, skill.Name);
return (
<div className="space-y-2.5">
{/* Skill Name Heading */}
<h3 className="text-xl font-bold text-base-content tracking-tight">
{skillName}
</h3>
{/* Attack Type & Effect Tag */}
{(attackTypeLabel || skillEffectLabel) && (
<div className="text-sm font-bold text-primary flex items-center gap-1.5">
<span>{attackTypeLabel}</span>
{skillEffectLabel && (
<span className="text-primary/70 font-medium">({skillEffectLabel})</span>
)}
</div>
)}
{/* Skill Subtitle with accent bar */}
<div className="flex items-center gap-2 text-sm font-bold text-primary uppercase tracking-wide">
<span className="w-1 h-3.5 bg-primary rounded-xs"></span>
<span>{skillName}</span>
</div>
{/* Description */}
<div
className="text-xs sm:text-sm leading-relaxed text-base-content/85"
dangerouslySetInnerHTML={{ __html: replaceByParam(descHtml, params) }}
/>
{/* Extra Effects Accordion */}
{Object.keys(extraList).length > 0 && (
<ExtraEffectList extras={extraList} locale={locale} />
)}
</div>
);
};
@@ -1,8 +1,8 @@
"use client";
import { PropsWithChildren, useContext } from "react";
import { ThemeContext } from "./themeContext";
export function ClientThemeWrapper({ children }: PropsWithChildren) {
const { theme } = useContext(ThemeContext);
return <div data-theme={theme} className="h-full">{children}</div>;
"use client";
import { PropsWithChildren, useContext } from "react";
import { ThemeContext } from "./themeContext";
export function ClientThemeWrapper({ children }: PropsWithChildren) {
const { theme } = useContext(ThemeContext);
return <div data-theme={theme} className="h-full">{children}</div>;
}
+2 -2
View File
@@ -1,2 +1,2 @@
export * from "./clientThemeWrapper"
export * from "./themeContext"
export * from "./clientThemeWrapper"
export * from "./themeContext"
+32 -40
View File
@@ -1,41 +1,33 @@
"use client";
import { createContext, PropsWithChildren, useEffect } from "react";
import useLocaleStore from "@/stores/localeStore";
interface ThemeContextType {
theme?: string;
changeTheme?: (nextTheme: string | null) => void;
}
export const ThemeContext = createContext<ThemeContextType>({});
export const ThemeProvider = ({ children }: PropsWithChildren) => {
const { theme, setTheme } = useLocaleStore()
useEffect(() => {
if (typeof window !== "undefined") {
const storedTheme = localStorage.getItem("theme");
if (storedTheme) setTheme(storedTheme);
}
}, [setTheme]);
const changeTheme = (nextTheme: string | null) => {
if (nextTheme) {
setTheme(nextTheme);
if (typeof window !== "undefined") {
localStorage.setItem("theme", nextTheme);
}
} else {
setTheme(theme === "winter" ? "night" : "winter");
if (typeof window !== "undefined") {
localStorage.setItem("theme", theme);
}
}
};
return (
<ThemeContext.Provider value={{ theme, changeTheme }}>
{children}
</ThemeContext.Provider>
);
"use client";
import { createContext, PropsWithChildren, useCallback } from "react";
import useLocaleStore from "@/stores/localeStore";
import { useShallow } from "zustand/react/shallow";
interface ThemeContextType {
theme?: string;
changeTheme?: (nextTheme: string | null) => void;
}
export const ThemeContext = createContext<ThemeContextType>({});
export const ThemeProvider = ({ children }: PropsWithChildren) => {
const { theme, setTheme } = useLocaleStore(
useShallow((state) => ({
theme: state.theme,
setTheme: state.setTheme,
}))
);
const changeTheme = useCallback((nextTheme: string | null) => {
if (nextTheme) {
setTheme(nextTheme);
} else {
setTheme(theme === "winter" ? "night" : "winter");
}
}, [theme, setTheme]);
return (
<ThemeContext.Provider value={{ theme, changeTheme }}>
{children}
</ThemeContext.Provider>
);
};
+220 -220
View File
@@ -1,220 +1,220 @@
export const listCurrentLanguage = {
en: { label: "English", flag: "🇬🇧" },
vi: { label: "Tiếng Việt", flag: "🇻🇳" },
ja: { label: "日本語", flag: "🇯🇵" },
ko: { label: "한국어", flag: "🇰🇷" },
zh: { label: "中文", flag: "🇨🇳" },
de: { label: "Deutsch", flag: "🇩🇪" },
es: { label: "Español", flag: "🇪🇸" },
fr: { label: "Français", flag: "🇫🇷" },
id: { label: "Bahasa Indonesia", flag: "🇮🇩" },
pt: { label: "Português", flag: "🇵🇹" },
ru: { label: "Русский", flag: "🇷🇺" },
th: { label: "ไทย", flag: "🇹🇭" }
};
export const listCurrentLanguageApi : Record<string, string> = {
ja: "jp",
ko: "kr",
en: "en",
vi: "vi",
zh: "cn",
de: "de",
es: "es",
fr: "fr",
id: "id",
pt: "pt",
ru: "ru",
th: "th"
};
export const mappingStats = <Record<string, {name: string, icon: string, unit: string, baseStat: string}> > {
"HPDelta": {
name:"HP",
icon:"spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "",
baseStat: "HP"
},
"AttackDelta": {
name:"ATK",
icon:"spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "",
baseStat: "ATK"
},
"HPAddedRatio": {
name:"HP",
icon:"spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "%",
baseStat: "HP"
},
"AttackAddedRatio": {
name:"ATK",
icon:"spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "%",
baseStat: "ATK"
},
"DefenceDelta": {
name:"DEF",
icon:"spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "",
baseStat: "DEF"
},
"DefenceAddedRatio": {
name:"DEF",
icon:"spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "%",
baseStat: "DEF"
},
"SpeedAddedRatio": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "%",
baseStat: "SPD"
},
"BaseSpeed": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
baseStat: "SPD"
},
"CriticalChanceBase": {
name:"CRIT Rate",
icon:"spriteoutput/ui/avatar/icon/IconCriticalChance.png",
unit: "%",
baseStat: "CRITRate"
},
"CriticalDamageBase": {
name:"CRIT DMG",
icon:"spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
unit: "%",
baseStat: "CRITDmg"
},
"HealRatioBase": {
name:"Outgoing Healing Boost",
icon:"spriteoutput/ui/avatar/icon/IconHealRatio.png",
unit: "%",
baseStat: "HealBoost"
},
"StatusProbabilityBase": {
name:"Effect Hit Rate",
icon:"spriteoutput/ui/avatar/icon/IconStatusProbability.png",
unit: "%",
baseStat: "EffectHitRate"
},
"StatusResistanceBase": {
name:"Effect RES",
icon:"spriteoutput/ui/avatar/icon/IconStatusResistance.png",
unit: "%",
baseStat: "EffectRES"
},
"BreakDamageAddedRatioBase": {
name:"Break Effect",
icon:"spriteoutput/ui/avatar/icon/IconBreakUp.png",
unit: "%",
baseStat: "BreakEffect"
},
"SpeedDelta": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
baseStat: "SPD"
},
"PhysicalAddedRatio": {
name:"Physical DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
unit: "%",
baseStat: "PhysicalAdd"
},
"FireAddedRatio": {
name:"Fire DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
unit: "%",
baseStat: "FireAdd"
},
"IceAddedRatio": {
name:"Ice DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
unit: "%",
baseStat: "IceAdd"
},
"ThunderAddedRatio": {
name:"Thunder DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
unit: "%",
baseStat: "ThunderAdd"
},
"WindAddedRatio": {
name:"Wind DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
unit: "%",
baseStat: "WindAdd"
},
"QuantumAddedRatio": {
name:"Quantum DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
unit: "%",
baseStat: "QuantumAdd"
},
"ImaginaryAddedRatio": {
name:"Imaginary DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
unit: "%",
baseStat: "ImaginaryAdd"
},
"ElationDamageAddedRatioBase": {
name:"Elation DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconJoy.png",
unit: "%",
baseStat: "ElationAdd"
},
"SPRatioBase": {
name:"Energy Regeneration Rate",
icon:"spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
unit: "%",
baseStat: "EnergyRate"
}
}
export const ratioStats = [
"HPAddedRatio",
"AttackAddedRatio",
"DefenceAddedRatio",
"SpeedAddedRatio",
]
export const mappingRelicSlot: Record<string, string> = {
"1": "HEAD",
"2": "HAND",
"3": "BODY",
"4": "FOOT",
"5": "NECK",
"6": "OBJECT",
}
export const themeColors: Record<string, { bg: string; bgHover: string; text: string; border: string }> = {
winter: {
bg: '#ffffff',
bgHover: '#f1f5f9',
text: '#3a4f6b',
border: '#cbd5e1'
},
night: {
bg: '#1d232a',
bgHover: '#2a323c',
text: '#cbcdd1',
border: '#3f3f46'
},
cupcake: {
bg: '#faf7f5',
bgHover: '#f3eae6',
text: '#281333',
border: '#e5d3cb'
},
coffee: {
bg: '#20161f',
bgHover: '#2a1d29',
text: '#c4a051',
border: '#3a2a36'
}
}
export const listCurrentLanguage = {
en: { label: "English", flag: "🇬🇧" },
vi: { label: "Tiếng Việt", flag: "🇻🇳" },
ja: { label: "日本語", flag: "🇯🇵" },
ko: { label: "한국어", flag: "🇰🇷" },
zh: { label: "中文", flag: "🇨🇳" },
de: { label: "Deutsch", flag: "🇩🇪" },
es: { label: "Español", flag: "🇪🇸" },
fr: { label: "Français", flag: "🇫🇷" },
id: { label: "Bahasa Indonesia", flag: "🇮🇩" },
pt: { label: "Português", flag: "🇵🇹" },
ru: { label: "Русский", flag: "🇷🇺" },
th: { label: "ไทย", flag: "🇹🇭" }
};
export const listCurrentLanguageApi : Record<string, string> = {
ja: "jp",
ko: "kr",
en: "en",
vi: "vi",
zh: "cn",
de: "de",
es: "es",
fr: "fr",
id: "id",
pt: "pt",
ru: "ru",
th: "th"
};
export const mappingStats = <Record<string, {name: string, icon: string, unit: string, baseStat: string}> > {
"HPDelta": {
name:"HP",
icon:"spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "",
baseStat: "HP"
},
"AttackDelta": {
name:"ATK",
icon:"spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "",
baseStat: "ATK"
},
"HPAddedRatio": {
name:"HP",
icon:"spriteoutput/ui/avatar/icon/IconMaxHP.png",
unit: "%",
baseStat: "HP"
},
"AttackAddedRatio": {
name:"ATK",
icon:"spriteoutput/ui/avatar/icon/IconAttack.png",
unit: "%",
baseStat: "ATK"
},
"DefenceDelta": {
name:"DEF",
icon:"spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "",
baseStat: "DEF"
},
"DefenceAddedRatio": {
name:"DEF",
icon:"spriteoutput/ui/avatar/icon/IconDefence.png",
unit: "%",
baseStat: "DEF"
},
"SpeedAddedRatio": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "%",
baseStat: "SPD"
},
"BaseSpeed": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
baseStat: "SPD"
},
"CriticalChanceBase": {
name:"CRIT Rate",
icon:"spriteoutput/ui/avatar/icon/IconCriticalChance.png",
unit: "%",
baseStat: "CRITRate"
},
"CriticalDamageBase": {
name:"CRIT DMG",
icon:"spriteoutput/ui/avatar/icon/IconCriticalDamage.png",
unit: "%",
baseStat: "CRITDmg"
},
"HealRatioBase": {
name:"Outgoing Healing Boost",
icon:"spriteoutput/ui/avatar/icon/IconHealRatio.png",
unit: "%",
baseStat: "HealBoost"
},
"StatusProbabilityBase": {
name:"Effect Hit Rate",
icon:"spriteoutput/ui/avatar/icon/IconStatusProbability.png",
unit: "%",
baseStat: "EffectHitRate"
},
"StatusResistanceBase": {
name:"Effect RES",
icon:"spriteoutput/ui/avatar/icon/IconStatusResistance.png",
unit: "%",
baseStat: "EffectRES"
},
"BreakDamageAddedRatioBase": {
name:"Break Effect",
icon:"spriteoutput/ui/avatar/icon/IconBreakUp.png",
unit: "%",
baseStat: "BreakEffect"
},
"SpeedDelta": {
name:"SPD",
icon:"spriteoutput/ui/avatar/icon/IconSpeed.png",
unit: "",
baseStat: "SPD"
},
"PhysicalAddedRatio": {
name:"Physical DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconPhysicalAddedRatio.png",
unit: "%",
baseStat: "PhysicalAdd"
},
"FireAddedRatio": {
name:"Fire DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconFireAddedRatio.png",
unit: "%",
baseStat: "FireAdd"
},
"IceAddedRatio": {
name:"Ice DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconIceAddedRatio.png",
unit: "%",
baseStat: "IceAdd"
},
"ThunderAddedRatio": {
name:"Thunder DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconThunderAddedRatio.png",
unit: "%",
baseStat: "ThunderAdd"
},
"WindAddedRatio": {
name:"Wind DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconWindAddedRatio.png",
unit: "%",
baseStat: "WindAdd"
},
"QuantumAddedRatio": {
name:"Quantum DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconQuantumAddedRatio.png",
unit: "%",
baseStat: "QuantumAdd"
},
"ImaginaryAddedRatio": {
name:"Imaginary DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconImaginaryAddedRatio.png",
unit: "%",
baseStat: "ImaginaryAdd"
},
"ElationDamageAddedRatioBase": {
name:"Elation DMG Boost",
icon:"spriteoutput/ui/avatar/icon/IconJoy.png",
unit: "%",
baseStat: "ElationAdd"
},
"SPRatioBase": {
name:"Energy Regeneration Rate",
icon:"spriteoutput/ui/avatar/icon/IconEnergyRecovery.png",
unit: "%",
baseStat: "EnergyRate"
}
}
export const ratioStats = [
"HPAddedRatio",
"AttackAddedRatio",
"DefenceAddedRatio",
"SpeedAddedRatio",
]
export const mappingRelicSlot: Record<string, string> = {
"1": "HEAD",
"2": "HAND",
"3": "BODY",
"4": "FOOT",
"5": "NECK",
"6": "OBJECT",
}
export const themeColors: Record<string, { bg: string; bgHover: string; text: string; border: string }> = {
winter: {
bg: '#ffffff',
bgHover: '#f1f5f9',
text: '#3a4f6b',
border: '#cbd5e1'
},
night: {
bg: '#1d232a',
bgHover: '#2a323c',
text: '#cbcdd1',
border: '#3f3f46'
},
cupcake: {
bg: '#faf7f5',
bgHover: '#f3eae6',
text: '#281333',
border: '#e5d3cb'
},
coffee: {
bg: '#20161f',
bgHover: '#2a1d29',
text: '#c4a051',
border: '#3a2a36'
}
}
+1147 -1147
View File
File diff suppressed because it is too large Load Diff
+104 -104
View File
@@ -1,105 +1,105 @@
"use client"
import { SendDataThroughProxy, SendDataToServer } from "@/lib/api/api"
import useConnectStore from "@/stores/connectStore"
import useUserDataStore from "@/stores/userDataStore"
import { converterToFreeSRJson } from "./converterToFreeSRJson"
import { psResponseSchema } from "@/zod"
import useGlobalStore from "@/stores/globalStore"
import { ActionResult, ExtraData, ProxyPayload, ProxyResponse, PSConnectType, PSResponse } from "@/types"
const getUrlQuery = (connectionType: PSConnectType | string, serverUrl: string): string => {
if (connectionType === PSConnectType.FireflyGo) return "http://localhost:21000/sync"
if (connectionType === PSConnectType.RobinSR) return "http://localhost:21000/srtools"
if (!serverUrl.startsWith("http://") && !serverUrl.startsWith("https://")) {
return `http://${serverUrl}`
}
return serverUrl
}
const handleProxyRequest = async (payload: ProxyPayload): Promise<ActionResult> => {
const response = await SendDataThroughProxy({
data: { ...payload, method: "POST" }
}) as ProxyResponse | Error
if (response instanceof Error) {
return { success: false, message: response.message }
}
if (response.error) {
return { success: false, message: response.error }
}
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return { success: false, message: "Invalid response schema" }
}
return { success: true, message: "" }
}
const handleDirectServerResponse = (
response: PSResponse | string,
setIsConnectPS: (val: boolean) => void,
onSuccess: (extraData?: ExtraData) => void
): ActionResult => {
if (typeof response === "string") {
setIsConnectPS(false)
return { success: false, message: response }
}
if (response.status !== 200) {
setIsConnectPS(false)
return { success: false, message: response.message }
}
setIsConnectPS(true)
onSuccess(response?.extra_data)
return { success: true, message: "" }
}
export const connectToPS = async (): Promise<ActionResult> => {
const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState()
const { setExtraData, setIsConnectPS } = useGlobalStore.getState()
if (connectionType === "Other" && privateType === "Server") {
return handleProxyRequest({ username, password, serverUrl, data: undefined })
}
const urlQuery = getUrlQuery(connectionType, serverUrl)
const response = await SendDataToServer(username, password, urlQuery, undefined)
return handleDirectServerResponse(response, setIsConnectPS, (extraData) => {
setExtraData(extraData)
})
}
export const syncDataToPS = async (): Promise<ActionResult> => {
const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState()
const { extraData, setIsConnectPS, setExtraData, isEnableChangePath, isEnableLua } = useGlobalStore.getState()
const { avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config } = useUserDataStore.getState()
const data = converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config)
if (connectionType === "Other" && privateType === "Server") {
return handleProxyRequest({ username, password, serverUrl, data })
}
const urlQuery = getUrlQuery(connectionType, serverUrl)
const payloadExtra: PSResponse['extra_data'] = structuredClone(extraData)
if (payloadExtra) {
if (!isEnableChangePath) payloadExtra.multi_path = undefined
if (!isEnableLua) payloadExtra.lua = null
}
const response = await SendDataToServer(username, password, urlQuery, data, payloadExtra)
return handleDirectServerResponse(response, setIsConnectPS, (responseExtraData) => {
const newData = structuredClone(responseExtraData)
if (newData) {
newData.lua = extraData?.lua || null
}
setExtraData(newData)
})
"use client"
import { SendDataThroughProxy, SendDataToServer } from "@/lib/api/api"
import useConnectStore from "@/stores/connectStore"
import useUserDataStore from "@/stores/userDataStore"
import { converterToFreeSRJson } from "./converterToFreeSRJson"
import { psResponseSchema } from "@/zod"
import useGlobalStore from "@/stores/globalStore"
import { ActionResult, ExtraData, ProxyPayload, ProxyResponse, PSConnectType, PSResponse } from "@/types"
const getUrlQuery = (connectionType: PSConnectType | string, serverUrl: string): string => {
if (connectionType === PSConnectType.FireflyGo) return "http://localhost:21000/sync"
if (connectionType === PSConnectType.RobinSR) return "http://localhost:21000/srtools"
if (!serverUrl.startsWith("http://") && !serverUrl.startsWith("https://")) {
return `http://${serverUrl}`
}
return serverUrl
}
const handleProxyRequest = async (payload: ProxyPayload): Promise<ActionResult> => {
const response = await SendDataThroughProxy({
data: { ...payload, method: "POST" }
}) as ProxyResponse | Error
if (response instanceof Error) {
return { success: false, message: response.message }
}
if (response.error) {
return { success: false, message: response.error }
}
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return { success: false, message: "Invalid response schema" }
}
return { success: true, message: "" }
}
const handleDirectServerResponse = (
response: PSResponse | string,
setIsConnectPS: (val: boolean) => void,
onSuccess: (extraData?: ExtraData) => void
): ActionResult => {
if (typeof response === "string") {
setIsConnectPS(false)
return { success: false, message: response }
}
if (response.status !== 200) {
setIsConnectPS(false)
return { success: false, message: response.message }
}
setIsConnectPS(true)
onSuccess(response?.extra_data)
return { success: true, message: "" }
}
export const connectToPS = async (): Promise<ActionResult> => {
const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState()
const { setExtraData, setIsConnectPS } = useGlobalStore.getState()
if (connectionType === "Other" && privateType === "Server") {
return handleProxyRequest({ username, password, serverUrl, data: undefined })
}
const urlQuery = getUrlQuery(connectionType, serverUrl)
const response = await SendDataToServer(username, password, urlQuery, undefined)
return handleDirectServerResponse(response, setIsConnectPS, (extraData) => {
setExtraData(extraData)
})
}
export const syncDataToPS = async (): Promise<ActionResult> => {
const { connectionType, privateType, serverUrl, username, password } = useConnectStore.getState()
const { extraData, setIsConnectPS, setExtraData, isEnableChangePath, isEnableLua } = useGlobalStore.getState()
const { avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config } = useUserDataStore.getState()
const data = converterToFreeSRJson(avatars, battle_type, moc_config, pf_config, as_config, ce_config, peak_config)
if (connectionType === "Other" && privateType === "Server") {
return handleProxyRequest({ username, password, serverUrl, data })
}
const urlQuery = getUrlQuery(connectionType, serverUrl)
const payloadExtra: PSResponse['extra_data'] = structuredClone(extraData)
if (payloadExtra) {
if (!isEnableChangePath) payloadExtra.multi_path = undefined
if (!isEnableLua) payloadExtra.lua = null
}
const response = await SendDataToServer(username, password, urlQuery, data, payloadExtra)
return handleDirectServerResponse(response, setIsConnectPS, (responseExtraData) => {
const newData = structuredClone(responseExtraData)
if (newData) {
newData.lua = extraData?.lua || null
}
setExtraData(newData)
})
}
+14 -14
View File
@@ -1,15 +1,15 @@
export function convertToRoman(num: number): string {
const roman: [number, string][] = [
[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'],
[100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'],
[10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']
];
let result = '';
for (const [val, sym] of roman) {
while (num >= val) {
result += sym;
num -= val;
}
}
return result;
export function convertToRoman(num: number): string {
const roman: [number, string][] = [
[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'],
[100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'],
[10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']
];
let result = '';
for (const [val, sym] of roman) {
while (num >= val) {
result += sym;
num -= val;
}
}
return result;
}
+119 -119
View File
@@ -1,89 +1,89 @@
import useConnectStore from "@/stores/connectStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { ASConfigStore, AvatarJson, AvatarStore, BattleConfigJson, CEConfigStore, FreeSRJson, LightconeJson, MOCConfigStore, PEAKConfigStore, PFConfigStore, PSConnectType, RelicJson } from "@/types";
export function converterToFreeSRJson(
avatars: Record<string, AvatarStore>,
battle_type: string,
moc_config: MOCConfigStore,
pf_config: PFConfigStore,
as_config: ASConfigStore,
ce_config: CEConfigStore,
peak_config: PEAKConfigStore,
): FreeSRJson {
const { skillConfig } = useDetailDataStore.getState()
const { connectionType } = useConnectStore.getState()
const lightcones: LightconeJson[] = []
const relics: RelicJson[] = []
let battleJson: BattleConfigJson
if (battle_type === "MOC") {
battleJson = {
battle_type: battle_type,
blessings: moc_config.blessings,
custom_stats: [],
cycle_count: moc_config.cycle_count,
stage_id: moc_config.stage_id,
path_resonance_id: 0,
monsters: moc_config.monsters,
}
} else if (battle_type === "PF") {
battleJson = {
battle_type: battle_type,
blessings: pf_config.blessings,
custom_stats: [],
cycle_count: pf_config.cycle_count,
stage_id: pf_config.stage_id,
path_resonance_id: 0,
monsters: pf_config.monsters,
}
} else if (battle_type === "AS") {
battleJson = {
battle_type: battle_type,
blessings: as_config.blessings,
custom_stats: [],
cycle_count: as_config.cycle_count,
stage_id: as_config.stage_id,
path_resonance_id: 0,
monsters: as_config.monsters,
}
} else if (battle_type === "CE") {
battleJson = {
battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT",
blessings: ce_config.blessings,
custom_stats: [],
cycle_count: ce_config.cycle_count,
stage_id: ce_config.stage_id,
path_resonance_id: 0,
monsters: ce_config.monsters,
}
} else if (battle_type === "PEAK") {
battleJson = {
battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT",
blessings: peak_config.blessings,
custom_stats: [],
cycle_count: peak_config.cycle_count,
stage_id: peak_config.stage_id,
path_resonance_id: 0,
monsters: peak_config.monsters,
}
} else {
battleJson = {
battle_type: battle_type,
blessings: [],
custom_stats: [],
cycle_count: 0,
stage_id: 0,
path_resonance_id: 0,
monsters: [],
}
}
const avatarsJson: { [key: string]: AvatarJson } = {}
let internalUidLightcone = 0
let internalUidRelic = 0
import useConnectStore from "@/stores/connectStore";
import useDetailDataStore from "@/stores/detailDataStore";
import { ASConfigStore, AvatarJson, AvatarStore, BattleConfigJson, CEConfigStore, FreeSRJson, LightconeJson, MOCConfigStore, PEAKConfigStore, PFConfigStore, PSConnectType, RelicJson } from "@/types";
export function converterToFreeSRJson(
avatars: Record<string, AvatarStore>,
battle_type: string,
moc_config: MOCConfigStore,
pf_config: PFConfigStore,
as_config: ASConfigStore,
ce_config: CEConfigStore,
peak_config: PEAKConfigStore,
): FreeSRJson {
const { skillConfig } = useDetailDataStore.getState()
const { connectionType } = useConnectStore.getState()
const lightcones: LightconeJson[] = []
const relics: RelicJson[] = []
let battleJson: BattleConfigJson
if (battle_type === "MOC") {
battleJson = {
battle_type: battle_type,
blessings: moc_config.blessings,
custom_stats: [],
cycle_count: moc_config.cycle_count,
stage_id: moc_config.stage_id,
path_resonance_id: 0,
monsters: moc_config.monsters,
}
} else if (battle_type === "PF") {
battleJson = {
battle_type: battle_type,
blessings: pf_config.blessings,
custom_stats: [],
cycle_count: pf_config.cycle_count,
stage_id: pf_config.stage_id,
path_resonance_id: 0,
monsters: pf_config.monsters,
}
} else if (battle_type === "AS") {
battleJson = {
battle_type: battle_type,
blessings: as_config.blessings,
custom_stats: [],
cycle_count: as_config.cycle_count,
stage_id: as_config.stage_id,
path_resonance_id: 0,
monsters: as_config.monsters,
}
} else if (battle_type === "CE") {
battleJson = {
battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT",
blessings: ce_config.blessings,
custom_stats: [],
cycle_count: ce_config.cycle_count,
stage_id: ce_config.stage_id,
path_resonance_id: 0,
monsters: ce_config.monsters,
}
} else if (battle_type === "PEAK") {
battleJson = {
battle_type: connectionType === PSConnectType.FireflyGo ? battle_type : "DEFAULT",
blessings: peak_config.blessings,
custom_stats: [],
cycle_count: peak_config.cycle_count,
stage_id: peak_config.stage_id,
path_resonance_id: 0,
monsters: peak_config.monsters,
}
} else {
battleJson = {
battle_type: battle_type,
blessings: [],
custom_stats: [],
cycle_count: 0,
stage_id: 0,
path_resonance_id: 0,
monsters: [],
}
}
const avatarsJson: { [key: string]: AvatarJson } = {}
let internalUidLightcone = 0
let internalUidRelic = 0
Object.entries(avatars ?? {}).forEach(([avatarId, avatar]) => {
if (!avatar) return
@@ -91,10 +91,10 @@ export function converterToFreeSRJson(
for (const [skillId, level] of Object.entries(avatar?.data?.skills || {})) {
if (skillConfig?.[skillId]) {
skillsByAnchorType[skillConfig[skillId].IndexSlot] = level > skillConfig[skillId].MaxLevel ? skillConfig[skillId].MaxLevel : level
}
}
avatarsJson[avatarId] = {
owner_uid: Number(avatar.owner_uid || 0),
}
}
avatarsJson[avatarId] = {
owner_uid: Number(avatar.owner_uid || 0),
avatar_id: Number(avatar.avatar_id || 0),
data: {
rank: Number(avatar.data?.rank || 0),
@@ -115,21 +115,21 @@ export function converterToFreeSRJson(
const newLightcone: LightconeJson = {
level: Number(currentProfile.lightcone.level || 0),
item_id: Number(currentProfile.lightcone.item_id || 0),
rank: Number(currentProfile.lightcone.rank || 0),
promotion: Number(currentProfile.lightcone.promotion || 0),
internal_uid: internalUidLightcone,
equip_avatar: Number(avatar.avatar_id || 0),
}
internalUidLightcone++
lightcones.push(newLightcone)
}
if (currentProfile.relics) {
["1", "2", "3", "4", "5", "6"].forEach(slot => {
const relic = currentProfile.relics[slot]
if (relic && relic.relic_id !== 0) {
const newRelic: RelicJson = {
level: Number(relic.level || 0),
rank: Number(currentProfile.lightcone.rank || 0),
promotion: Number(currentProfile.lightcone.promotion || 0),
internal_uid: internalUidLightcone,
equip_avatar: Number(avatar.avatar_id || 0),
}
internalUidLightcone++
lightcones.push(newLightcone)
}
if (currentProfile.relics) {
["1", "2", "3", "4", "5", "6"].forEach(slot => {
const relic = currentProfile.relics[slot]
if (relic && relic.relic_id !== 0) {
const newRelic: RelicJson = {
level: Number(relic.level || 0),
relic_id: Number(relic.relic_id || 0),
relic_set_id: Number(relic.relic_set_id || 0),
main_affix_id: Number(relic.main_affix_id || 0),
@@ -137,18 +137,18 @@ export function converterToFreeSRJson(
internal_uid: internalUidRelic,
equip_avatar: Number(avatar.avatar_id || 0),
}
internalUidRelic++
relics.push(newRelic)
}
})
}
})
return {
lightcones,
relics,
avatars: avatarsJson,
battle_config: battleJson,
}
internalUidRelic++
relics.push(newRelic)
}
})
}
})
return {
lightcones,
relics,
avatars: avatarsJson,
battle_config: battleJson,
}
}
+61 -61
View File
@@ -1,62 +1,62 @@
import { listCurrentLanguageApi } from "@/constant/constant";
import { AvatarDetail } from "@/types";
import { useTranslations } from "next-intl"
type TFunc = ReturnType<typeof useTranslations>
function cleanText(text: string): string {
if (!text) return ""
return text.replace(/<unbreak>(.*?)<\/unbreak>/g, "$1")
}
export function getNameChar(
locale: string,
t: TFunc,
data: AvatarDetail | undefined
): string {
if (!data) return "";
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguageApi, locale)) {
return "";
}
const langKey = listCurrentLanguageApi[locale as keyof typeof listCurrentLanguageApi].toLowerCase();
let text = data.Name[langKey] ?? "";
if (!text) {
text = data.Name["en"] ?? "";
}
if (data.ID > 8000) {
text = `${t("trailblazer")} • ${t(data?.BaseType?.toLowerCase() ?? "")}`;
}
return cleanText(text)
}
export function getLocaleName(locale: string, data: Record<string, string> | undefined | null): string {
if (!data) {
return ""
}
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguageApi, locale)) {
return ""
}
const langKey = listCurrentLanguageApi[locale as keyof typeof listCurrentLanguageApi].toLowerCase();
let text = data[langKey] ?? "";
if (!text) {
text = data["en"] ?? "";
}
return cleanText(text)
}
export function parseRuby(text: string): string {
const rubyRegex = /\{RUBY_B#(.*?)\}(.*?)\{RUBY_E#\}/gs;
return text.replace(rubyRegex, (_match, furigana, kanji) => {
return `<ruby>${kanji}<rt>${furigana}</rt></ruby>`;
});
import { listCurrentLanguageApi } from "@/constant/constant";
import { AvatarDetail } from "@/types";
import { useTranslations } from "next-intl"
type TFunc = ReturnType<typeof useTranslations>
function cleanText(text: string): string {
if (!text) return ""
return text.replace(/<unbreak>(.*?)<\/unbreak>/g, "$1")
}
export function getNameChar(
locale: string,
t: TFunc,
data: AvatarDetail | undefined
): string {
if (!data) return "";
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguageApi, locale)) {
return "";
}
const langKey = listCurrentLanguageApi[locale as keyof typeof listCurrentLanguageApi].toLowerCase();
let text = data.Name[langKey] ?? "";
if (!text) {
text = data.Name["en"] ?? "";
}
if (data.ID > 8000) {
text = `${t("trailblazer")} • ${t(data?.BaseType?.toLowerCase() ?? "")}`;
}
return cleanText(text)
}
export function getLocaleName(locale: string, data: Record<string, string> | undefined | null): string {
if (!data) {
return ""
}
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguageApi, locale)) {
return ""
}
const langKey = listCurrentLanguageApi[locale as keyof typeof listCurrentLanguageApi].toLowerCase();
let text = data[langKey] ?? "";
if (!text) {
text = data["en"] ?? "";
}
return cleanText(text)
}
export function parseRuby(text: string): string {
const rubyRegex = /\{RUBY_B#(.*?)\}(.*?)\{RUBY_E#\}/gs;
return text.replace(rubyRegex, (_match, furigana, kanji) => {
return `<ruby>${kanji}<rt>${furigana}</rt></ruby>`;
});
}
+19 -19
View File
@@ -1,23 +1,23 @@
import { AvatarDetail } from "@/types";
export function getSkillTree(avatarSelected: AvatarDetail | null, enhanced: string) {
if (!avatarSelected) return null;
if (enhanced != "" && !!avatarSelected?.Enhanced?.[enhanced]?.SkillTrees) {
return Object.values(avatarSelected?.Enhanced?.[enhanced]?.SkillTrees).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>)
}
import { AvatarDetail } from "@/types";
export function getSkillTree(avatarSelected: AvatarDetail | null, enhanced: string) {
if (!avatarSelected) return null;
if (enhanced != "" && !!avatarSelected?.Enhanced?.[enhanced]?.SkillTrees) {
return Object.values(avatarSelected?.Enhanced?.[enhanced]?.SkillTrees).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>)
}
return Object.values(avatarSelected?.SkillTrees ?? {}).reduce((acc, dataPointEntry) => {
const firstEntry = Object.values(dataPointEntry)[0];
if (firstEntry) {
acc[firstEntry.PointID] = firstEntry.MaxLevel;
}
return acc;
}, {} as Record<string, number>);
}
}
return acc;
}, {} as Record<string, number>);
}
+10 -8
View File
@@ -1,8 +1,10 @@
export * from "./getName"
export * from "./replaceByParam"
export * from "./converterToAvatarStore"
export * from "./calcData"
export * from "./random"
export * from "./json"
export * from "./convertData"
export * from "./connect"
export * from "./getName";
export * from "./replaceByParam";
export * from "./converterToAvatarStore";
export * from "./converterToFreeSRJson";
export * from "./getSkillTree";
export * from "./calcData";
export * from "./random";
export * from "./json";
export * from "./convertData";
export * from "./connect";
+23 -23
View File
@@ -8,27 +8,27 @@ export function randomPartition(sum: number, parts: number): number[] {
const raw = Array.from({ length: parts }, () => Math.random());
const total = raw.reduce((a, b) => a + b, 0);
const result = raw.map(r => Math.floor((r / total) * (sum - parts)) + 1);
let diff = sum - result.reduce((a, b) => a + b, 0);
while (diff !== 0) {
for (let i = 0; i < result.length && diff !== 0; i++) {
if (diff > 0) {
result[i]++;
diff--;
} else if (result[i] > 1) {
result[i]--;
diff++;
}
}
}
return result;
}
export function randomStep(x: number): number {
let total = 0;
for (let i = 0; i < x; i++) {
total += Math.floor(Math.random() * 3);
}
return total;
const result = raw.map(r => Math.floor((r / total) * (sum - parts)) + 1);
let diff = sum - result.reduce((a, b) => a + b, 0);
while (diff !== 0) {
for (let i = 0; i < result.length && diff !== 0; i++) {
if (diff > 0) {
result[i]++;
diff--;
} else if (result[i] > 1) {
result[i]--;
diff++;
}
}
}
return result;
}
export function randomStep(x: number): number {
let total = 0;
for (let i = 0; i < x; i++) {
total += Math.floor(Math.random() * 3);
}
return total;
}
+34 -34
View File
@@ -1,39 +1,39 @@
const formatValue = (value: number, format: string, floatDigits?: string, hasPercent?: boolean): string => {
if (format.startsWith('f')) {
const digits = parseInt(floatDigits || "1", 10);
const num = hasPercent ? value * 100 : value;
return `${num.toFixed(digits)}${hasPercent ? "%" : ""}`;
}
if (format === 'i') {
const num = hasPercent ? value * 100 : value;
return `${Math.round(num)}${hasPercent ? "%" : ""}`;
}
return String(value);
};
const formatValue = (value: number, format: string, floatDigits?: string, hasPercent?: boolean): string => {
if (format.startsWith('f')) {
const digits = parseInt(floatDigits || "1", 10);
const num = hasPercent ? value * 100 : value;
return `${num.toFixed(digits)}${hasPercent ? "%" : ""}`;
}
if (format === 'i') {
const num = hasPercent ? value * 100 : value;
return `${Math.round(num)}${hasPercent ? "%" : ""}`;
}
return String(value);
};
export function replaceByParam(desc?: string, params: number[] = []): string {
if (!desc) return "";
const PARAM_REGEX = /#(\d+)\[(f(\d+)|i)\](%)?/g;
const processor = (_match: string, index: string, format: string, digits?: string, percent?: string): string => {
const i = parseInt(index, 10) - 1;
const val = params[i];
return val !== undefined ? formatValue(val, format, digits, !!percent) : "";
};
let result = desc.replace(/<color=(#[0-9a-fA-F]{8})>(.*?)<\/color>/g, (_, color, inner) => {
const processedInner = inner.replace(PARAM_REGEX, processor);
return `<span style="color: ${color}">${processedInner}</span>`;
});
result = result.replace(/<unbreak>(.*?)<\/unbreak>/g, (_, inner) => {
return inner.replace(PARAM_REGEX, processor);
});
result = result.replace(PARAM_REGEX, processor);
return result.split("\\n").join("<br/>");
const processor = (_match: string, index: string, format: string, digits?: string, percent?: string): string => {
const i = parseInt(index, 10) - 1;
const val = params[i];
return val !== undefined ? formatValue(val, format, digits, !!percent) : "";
};
let result = desc.replace(/<color=(#[0-9a-fA-F]{8})>(.*?)<\/color>/g, (_, color, inner) => {
const processedInner = inner.replace(PARAM_REGEX, processor);
return `<span style="color: ${color}">${processedInner}</span>`;
});
result = result.replace(/<unbreak>(.*?)<\/unbreak>/g, (_, inner) => {
return inner.replace(PARAM_REGEX, processor);
});
result = result.replace(PARAM_REGEX, processor);
return result.split("\\n").join("<br/>");
}
+1 -1
View File
@@ -1 +1 @@
export * from "./useChangeTheme";
export * from "./useChangeTheme";
+3 -3
View File
@@ -1,4 +1,4 @@
import { useContext } from "react";
import { ThemeContext } from "@/components/themeController";
import { useContext } from "react";
import { ThemeContext } from "@/components/themeController";
export const useChangeTheme = () => useContext(ThemeContext);
+140 -141
View File
@@ -1,142 +1,141 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ASGroupDetail, ChangelogItemType, AvatarDetail, FreeSRJson, LightConeDetail, MOCGroupDetail, MonsterDetail, PeakGroupDetail, PFGroupDetail, PSResponse, RelicSetDetail } from "@/types";
import axios from 'axios';
import { psResponseSchema } from "@/zod";
import { ExtraData, Metadata } from "@/types";
export async function getMetadataApi(): Promise<Metadata> {
try {
const res = await axios.get<Metadata>(`/api/data/metadata`);
return res.data as Metadata;
} catch (error: unknown) {
console.error('Failed to fetch metadata:', error);
return {
BaseType: {},
DamageType: {},
MainAffix: {},
SubAffix: {},
SkillConfig: {},
Stage: {},
HardLevelConfig: {},
EliteConfig: {}
};
}
}
export async function getAvatarListApi(): Promise<Record<string, AvatarDetail>> {
try {
const res = await axios.get<Record<string, AvatarDetail>>(`/api/data/avatar`);
return res.data;
} catch (error) {
console.error('Failed to fetch Avatars:', error);
return {};
}
}
export async function getLightconeListApi(): Promise<Record<string, LightConeDetail>> {
try {
const res = await axios.get<Record<string, LightConeDetail>>(`/api/data/lightcone`);
return res.data;
} catch (error) {
console.error('Failed to fetch lightcones:', error);
return {};
}
}
export async function getRelicSetListApi(): Promise<Record<string, RelicSetDetail>> {
try {
const res = await axios.get<Record<string, RelicSetDetail>>(`/api/data/relic`);
return res.data;
} catch (error) {
console.error('Failed to fetch relics:', error);
return {};
}
}
export async function getMonsterListApi(): Promise<Record<string, MonsterDetail>> {
try {
const res = await axios.get<Record<string, MonsterDetail>>(`/api/data/monster`);
return res.data;
} catch (error) {
console.error('Failed to fetch monster:', error);
return {};
}
}
export async function getASEventListApi(): Promise<Record<string, ASGroupDetail>> {
try {
const res = await axios.get<Record<string, ASGroupDetail>>(`/api/data/as`);
return res.data;
} catch (error) {
console.error('Failed to fetch AS:', error);
return {};
}
}
export async function getPFEventListApi(): Promise<Record<string, PFGroupDetail>> {
try {
const res = await axios.get<Record<string, PFGroupDetail>>(`/api/data/pf`);
return res.data;
} catch (error) {
console.error('Failed to fetch PF:', error);
return {};
}
}
export async function getMOCEventListApi(): Promise<Record<string, MOCGroupDetail>> {
try {
const res = await axios.get<Record<string, MOCGroupDetail>>(`/api/data/moc`);
return res.data;
} catch (error) {
console.error('Failed to fetch MOC:', error);
return {};
}
}
export async function getPeakEventListApi(): Promise<Record<string, PeakGroupDetail>> {
try {
const res = await axios.get<Record<string, PeakGroupDetail>>(`/api/data/peak`);
return res.data;
} catch (error) {
console.error('Failed to fetch peak:', error);
return {};
}
}
export async function getChangelog(): Promise<ChangelogItemType[]> {
try {
const res = await axios.get<ChangelogItemType[]>(`/api/data/changelog`);
return res.data;
} catch (error) {
console.error('Failed to fetch monster:', error);
return [];
}
}
export async function SendDataToServer(
username: string,
password: string,
serverUrl: string,
data?: FreeSRJson,
extraData?: ExtraData
): Promise<PSResponse | string> {
try {
const response = await axios.post(`${serverUrl}`, { username, password, data, extra_data: extraData })
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return "Invalid response schema";
}
return parsed.data;
} catch (error: any) {
return error?.message || "Unknown error";
}
}
export async function SendDataThroughProxy({ data }: { data: any }) {
try {
const response = await axios.post(`/api/proxy`, { ...data })
return response.data;
} catch (error: any) {
return error;
}
import { ASGroupDetail, ChangelogItemType, AvatarDetail, FreeSRJson, LightConeDetail, MOCGroupDetail, MonsterDetail, PeakGroupDetail, PFGroupDetail, PSResponse, RelicSetDetail } from "@/types";
import axios from 'axios';
import { psResponseSchema } from "@/zod";
import { ExtraData, Metadata } from "@/types";
export async function getMetadataApi(): Promise<Metadata> {
try {
const res = await axios.get<Metadata>(`/api/data/metadata`);
return res.data as Metadata;
} catch (error: unknown) {
console.error('Failed to fetch metadata:', error);
return {
BaseType: {},
DamageType: {},
MainAffix: {},
SubAffix: {},
SkillConfig: {},
Stage: {},
HardLevelConfig: {},
EliteConfig: {}
};
}
}
export async function getAvatarListApi(): Promise<Record<string, AvatarDetail>> {
try {
const res = await axios.get<Record<string, AvatarDetail>>(`/api/data/avatar`);
return res.data;
} catch (error) {
console.error('Failed to fetch Avatars:', error);
return {};
}
}
export async function getLightconeListApi(): Promise<Record<string, LightConeDetail>> {
try {
const res = await axios.get<Record<string, LightConeDetail>>(`/api/data/lightcone`);
return res.data;
} catch (error) {
console.error('Failed to fetch lightcones:', error);
return {};
}
}
export async function getRelicSetListApi(): Promise<Record<string, RelicSetDetail>> {
try {
const res = await axios.get<Record<string, RelicSetDetail>>(`/api/data/relic`);
return res.data;
} catch (error) {
console.error('Failed to fetch relics:', error);
return {};
}
}
export async function getMonsterListApi(): Promise<Record<string, MonsterDetail>> {
try {
const res = await axios.get<Record<string, MonsterDetail>>(`/api/data/monster`);
return res.data;
} catch (error) {
console.error('Failed to fetch monster:', error);
return {};
}
}
export async function getASEventListApi(): Promise<Record<string, ASGroupDetail>> {
try {
const res = await axios.get<Record<string, ASGroupDetail>>(`/api/data/as`);
return res.data;
} catch (error) {
console.error('Failed to fetch AS:', error);
return {};
}
}
export async function getPFEventListApi(): Promise<Record<string, PFGroupDetail>> {
try {
const res = await axios.get<Record<string, PFGroupDetail>>(`/api/data/pf`);
return res.data;
} catch (error) {
console.error('Failed to fetch PF:', error);
return {};
}
}
export async function getMOCEventListApi(): Promise<Record<string, MOCGroupDetail>> {
try {
const res = await axios.get<Record<string, MOCGroupDetail>>(`/api/data/moc`);
return res.data;
} catch (error) {
console.error('Failed to fetch MOC:', error);
return {};
}
}
export async function getPeakEventListApi(): Promise<Record<string, PeakGroupDetail>> {
try {
const res = await axios.get<Record<string, PeakGroupDetail>>(`/api/data/peak`);
return res.data;
} catch (error) {
console.error('Failed to fetch peak:', error);
return {};
}
}
export async function getChangelog(): Promise<ChangelogItemType[]> {
try {
const res = await axios.get<ChangelogItemType[]>(`/api/data/changelog`);
return res.data;
} catch (error) {
console.error('Failed to fetch changelog:', error);
return [];
}
}
export async function SendDataToServer(
username: string,
password: string,
serverUrl: string,
data?: FreeSRJson,
extraData?: ExtraData
): Promise<PSResponse | string> {
try {
const response = await axios.post(`${serverUrl}`, { username, password, data, extra_data: extraData })
const parsed = psResponseSchema.safeParse(response.data)
if (!parsed.success) {
return "Invalid response schema";
}
return parsed.data;
} catch (error: unknown) {
return (error as Error)?.message || "Unknown error";
}
}
export async function SendDataThroughProxy({ data }: { data: Record<string, unknown> | unknown }) {
try {
const response = await axios.post(`/api/proxy`, typeof data === "object" && data !== null ? { ...data } : data)
return response.data;
} catch (error: unknown) {
return error;
}
}
+37 -37
View File
@@ -1,38 +1,38 @@
import { readFileSync, readdirSync } from "fs"
import path from "path"
type CacheItem = {
buf: Uint8Array
type: "json" | "br"
}
const cache = new Map<string, CacheItem>()
const dir = path.join(process.cwd(), "data")
for (const f of readdirSync(dir)) {
const file = path.join(dir, f)
if (f.endsWith(".json.br")) {
const name = f.replace(".json.br", "")
const buf = new Uint8Array(readFileSync(file))
cache.set(name, {
buf,
type: "br"
})
}
if (f.endsWith(".json")) {
const name = f.replace(".json", "")
const buf = new Uint8Array(readFileSync(file))
cache.set(name, {
buf,
type: "json"
})
}
}
export function getDataCache(name: string) {
return cache.get(name)
import { readFileSync, readdirSync } from "fs"
import path from "path"
type CacheItem = {
buf: Uint8Array
type: "json" | "br"
}
const cache = new Map<string, CacheItem>()
const dir = path.join(process.cwd(), "data")
for (const f of readdirSync(dir)) {
const file = path.join(dir, f)
if (f.endsWith(".json.br")) {
const name = f.replace(".json.br", "")
const buf = new Uint8Array(readFileSync(file))
cache.set(name, {
buf,
type: "br"
})
}
if (f.endsWith(".json")) {
const name = f.replace(".json", "")
const buf = new Uint8Array(readFileSync(file))
cache.set(name, {
buf,
type: "json"
})
}
}
export function getDataCache(name: string) {
return cache.get(name)
}
+9 -9
View File
@@ -1,10 +1,10 @@
export * from "./useFetchMetaData";
export * from "./useFetchAvatarData";
export * from "./useFetchLightconeData";
export * from "./useFetchRelicData";
export * from "./useFetchMonsterData";
export * from "./useFetchPFData";
export * from "./useFetchMOCData";
export * from "./useFetchASData";
export * from "./useFetchPEAKData";
export * from "./useFetchMetaData";
export * from "./useFetchAvatarData";
export * from "./useFetchLightconeData";
export * from "./useFetchRelicData";
export * from "./useFetchMonsterData";
export * from "./useFetchPFData";
export * from "./useFetchMOCData";
export * from "./useFetchASData";
export * from "./useFetchPEAKData";
export * from "./useFetchChangelog";
+29 -26
View File
@@ -1,26 +1,29 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getASEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore'
export const useFetchASGroupData = () => {
const { setMapAS } = useDetailDataStore()
const query = useQuery({
queryKey: ['ASGroupData'],
queryFn: getASEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data) {
setMapAS(query.data)
} else if (query.error) {
toast.error("Failed to load ASGroup data")
}
}, [query.data, query.error, setMapAS])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getASEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore'
import { useShallow } from 'zustand/react/shallow'
export const useFetchASGroupData = () => {
const { setMapAS } = useDetailDataStore(
useShallow((state) => ({ setMapAS: state.setMapAS }))
)
const query = useQuery({
queryKey: ['ASGroupData'],
queryFn: getASEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data) {
setMapAS(query.data)
} else if (query.error) {
toast.error("Failed to load ASGroup data")
}
}, [query.data, query.error, setMapAS])
return query
}
+57 -42
View File
@@ -1,43 +1,58 @@
/* eslint-disable react-hooks/exhaustive-deps */
"use client"
import { useQuery } from '@tanstack/react-query'
import { getAvatarListApi } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore'
import useCurrentDataStore from '@/stores/currentDataStore'
import useUserDataStore from '@/stores/userDataStore'
import { converterToAvatarStore } from '@/helper'
export const useFetchAvatarData = () => {
const { setMapAvatar, mapAvatar } = useDetailDataStore()
const { setAvatar, avatars } = useUserDataStore()
const { avatarSelected, setAvatarSelected } = useCurrentDataStore()
const query = useQuery({
queryKey: ['AvatarData'],
queryFn: getAvatarListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
const listAvatarId = Object.keys(avatars)
const listAvatarNotExist = Object.entries(mapAvatar).filter(([avatarId]) => !listAvatarId.includes(avatarId))
const avatarStore = converterToAvatarStore(Object.fromEntries(listAvatarNotExist))
if (Object.keys(avatarStore).length === 0) return
for (const avatar of Object.values(avatarStore)) {
setAvatar(avatar)
}
}, [mapAvatar])
useEffect(() => {
if (query.data) {
setMapAvatar(query.data)
if (!avatarSelected) {
setAvatarSelected(Object.values(query.data)[0])
}
}
if (query.error) toast.error("Failed to load Avatar data")
}, [query.data, query.error, setMapAvatar, avatarSelected, setAvatarSelected])
return query
"use client"
import { useQuery } from '@tanstack/react-query'
import { getAvatarListApi } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore'
import useCurrentDataStore from '@/stores/currentDataStore'
import useUserDataStore from '@/stores/userDataStore'
import { converterToAvatarStore } from '@/helper'
import { useShallow } from 'zustand/react/shallow'
export const useFetchAvatarData = () => {
const { setMapAvatar, mapAvatar } = useDetailDataStore(
useShallow((state) => ({
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({
queryKey: ['AvatarData'],
queryFn: getAvatarListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (!mapAvatar || Object.keys(mapAvatar).length === 0) return
const listAvatarId = Object.keys(avatars)
const listAvatarNotExist = Object.entries(mapAvatar).filter(([avatarId]) => !listAvatarId.includes(avatarId))
if (listAvatarNotExist.length === 0) return
const avatarStore = converterToAvatarStore(Object.fromEntries(listAvatarNotExist))
if (Object.keys(avatarStore).length === 0) return
setAvatars({ ...avatars, ...avatarStore })
}, [mapAvatar, avatars, setAvatars])
useEffect(() => {
if (query.data) {
setMapAvatar(query.data)
if (!avatarSelected) {
setAvatarSelected(Object.values(query.data)[0])
}
}
if (query.error) toast.error("Failed to load Avatar data")
}, [query.data, query.error, setMapAvatar, avatarSelected, setAvatarSelected])
return query
}
+41 -32
View File
@@ -1,32 +1,41 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getChangelog } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useModelStore from '@/stores/modelStore'
import useLocaleStore from '@/stores/localeStore'
export const useFetchChangelog = () => {
const { currentVersion, setChangelog, setCurrentVersion } = useLocaleStore()
const { setIsChangelog } = useModelStore()
const query = useQuery({
queryKey: ['changelog'],
queryFn: getChangelog,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setChangelog(query.data)
if (query.data?.[0] && query.data[0].version != currentVersion) {
setIsChangelog(true)
setCurrentVersion(query.data[0].version)
}
} else if (query.error) {
toast.error("Failed to load changelog data")
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query.data, query.error, setChangelog, setCurrentVersion, setIsChangelog])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getChangelog } from '@/lib/api'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useModelStore from '@/stores/modelStore'
import useLocaleStore from '@/stores/localeStore'
import { useShallow } from 'zustand/react/shallow'
export const useFetchChangelog = () => {
const { currentVersion, setChangelog, setCurrentVersion } = useLocaleStore(
useShallow((state) => ({
currentVersion: state.currentVersion,
setChangelog: state.setChangelog,
setCurrentVersion: state.setCurrentVersion,
}))
)
const { setIsChangelog } = useModelStore(
useShallow((state) => ({ setIsChangelog: state.setIsChangelog }))
)
const query = useQuery({
queryKey: ['changelog'],
queryFn: getChangelog,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setChangelog(query.data)
if (query.data?.[0] && query.data[0].version != currentVersion) {
setIsChangelog(true)
setCurrentVersion(query.data[0].version)
}
} else if (query.error) {
toast.error("Failed to load changelog data")
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query.data, query.error, setChangelog, setCurrentVersion, setIsChangelog])
return query
}
+28 -25
View File
@@ -1,25 +1,28 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getLightconeListApi } from '@/lib/api';
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore';
export const useFetchLightconeData = () => {
const { setMapLightCone } = useDetailDataStore()
const query = useQuery({
queryKey: ['lightconeData'],
queryFn: getLightconeListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapLightCone(query.data)
} else if (query.error) {
toast.error("Failed to load lightcone data")
}
}, [query.data, query.error, setMapLightCone])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getLightconeListApi } from '@/lib/api';
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import useDetailDataStore from '@/stores/detailDataStore';
import { useShallow } from 'zustand/react/shallow';
export const useFetchLightconeData = () => {
const { setMapLightCone } = useDetailDataStore(
useShallow((state) => ({ setMapLightCone: state.setMapLightCone }))
)
const query = useQuery({
queryKey: ['lightconeData'],
queryFn: getLightconeListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapLightCone(query.data)
} else if (query.error) {
toast.error("Failed to load lightcone data")
}
}, [query.data, query.error, setMapLightCone])
return query
}
+28 -25
View File
@@ -1,25 +1,28 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getMOCEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchMOCGroupData = () => {
const { setMapMoc } = useDetailDataStore()
const query = useQuery({
queryKey: ['MOCGroupData'],
queryFn: getMOCEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapMoc(query.data)
} else if (query.error) {
toast.error("Failed to load MOCGroup data")
}
}, [query.data, query.error, setMapMoc])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getMOCEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
import { useShallow } from 'zustand/react/shallow'
export const useFetchMOCGroupData = () => {
const { setMapMoc } = useDetailDataStore(
useShallow((state) => ({ setMapMoc: state.setMapMoc }))
)
const query = useQuery({
queryKey: ['MOCGroupData'],
queryFn: getMOCEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapMoc(query.data)
} else if (query.error) {
toast.error("Failed to load MOCGroup data")
}
}, [query.data, query.error, setMapMoc])
return query
}
+38 -33
View File
@@ -1,33 +1,38 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import { getMetadataApi } from '@/lib/api'
import useCurrentDataStore from '@/stores/currentDataStore'
import useDetailDataStore from '@/stores/detailDataStore'
export const useFetchConfigData = () => {
const { setMetaData } = useDetailDataStore()
const { setSettingData } = useCurrentDataStore()
const query = useQuery({
queryKey: ['initialConfigData'],
queryFn: async () => {
const metaData = await getMetadataApi()
return { metaData }
},
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setSettingData(query.data.metaData)
setMetaData(query.data.metaData)
}
else if (query.error) {
toast.error("Failed to load initial config data")
}
}, [query.data, query.error, setMetaData, setSettingData])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { useEffect } from 'react'
import { toast } from 'react-toastify'
import { getMetadataApi } from '@/lib/api'
import useCurrentDataStore from '@/stores/currentDataStore'
import useDetailDataStore from '@/stores/detailDataStore'
import { useShallow } from 'zustand/react/shallow'
export const useFetchConfigData = () => {
const { setMetaData } = useDetailDataStore(
useShallow((state) => ({ setMetaData: state.setMetaData }))
)
const { setSettingData } = useCurrentDataStore(
useShallow((state) => ({ setSettingData: state.setSettingData }))
)
const query = useQuery({
queryKey: ['initialConfigData'],
queryFn: async () => {
const metaData = await getMetadataApi()
return { metaData }
},
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setSettingData(query.data.metaData)
setMetaData(query.data.metaData)
}
else if (query.error) {
toast.error("Failed to load initial config data")
}
}, [query.data, query.error, setMetaData, setSettingData])
return query
}
+28 -25
View File
@@ -1,25 +1,28 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getMonsterListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchMonsterData = () => {
const { setMapMonster } = useDetailDataStore()
const query = useQuery({
queryKey: ['MonsterData'],
queryFn: getMonsterListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapMonster(query.data)
} else if (query.error) {
toast.error("Failed to load Monster data")
}
}, [query.data, query.error, setMapMonster])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getMonsterListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
import { useShallow } from 'zustand/react/shallow'
export const useFetchMonsterData = () => {
const { setMapMonster } = useDetailDataStore(
useShallow((state) => ({ setMapMonster: state.setMapMonster }))
)
const query = useQuery({
queryKey: ['MonsterData'],
queryFn: getMonsterListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapMonster(query.data)
} else if (query.error) {
toast.error("Failed to load Monster data")
}
}, [query.data, query.error, setMapMonster])
return query
}
+28 -25
View File
@@ -1,25 +1,28 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getPeakEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchPeakGroupData = () => {
const { setMapPeak } = useDetailDataStore()
const query = useQuery({
queryKey: ['PeakGroupData'],
queryFn: getPeakEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapPeak(query.data)
} else if (query.error) {
toast.error("Failed to load PeakGroup data")
}
}, [query.data, query.error, setMapPeak])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getPeakEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
import { useShallow } from 'zustand/react/shallow'
export const useFetchPeakGroupData = () => {
const { setMapPeak } = useDetailDataStore(
useShallow((state) => ({ setMapPeak: state.setMapPeak }))
)
const query = useQuery({
queryKey: ['PeakGroupData'],
queryFn: getPeakEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapPeak(query.data)
} else if (query.error) {
toast.error("Failed to load PeakGroup data")
}
}, [query.data, query.error, setMapPeak])
return query
}
+28 -25
View File
@@ -1,25 +1,28 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getPFEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchPFGroupData = () => {
const { setMapPF } = useDetailDataStore()
const query = useQuery({
queryKey: ['PFGroupData'],
queryFn: getPFEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapPF(query.data)
} else if (query.error) {
toast.error("Failed to load PFGroup data")
}
}, [query.data, query.error, setMapPF])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getPFEventListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
import { useShallow } from 'zustand/react/shallow'
export const useFetchPFGroupData = () => {
const { setMapPF } = useDetailDataStore(
useShallow((state) => ({ setMapPF: state.setMapPF }))
)
const query = useQuery({
queryKey: ['PFGroupData'],
queryFn: getPFEventListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapPF(query.data)
} else if (query.error) {
toast.error("Failed to load PFGroup data")
}
}, [query.data, query.error, setMapPF])
return query
}
+28 -25
View File
@@ -1,25 +1,28 @@
"use client"
import { useQuery } from '@tanstack/react-query'
import { getRelicSetListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
export const useFetchRelicSetData = () => {
const { setMapRelicSet } = useDetailDataStore()
const query = useQuery({
queryKey: ['RelicSetData'],
queryFn: getRelicSetListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapRelicSet(query.data)
} else if (query.error) {
toast.error("Failed to load RelicSet data")
}
}, [query.data, query.error, setMapRelicSet])
return query
}
"use client"
import { useQuery } from '@tanstack/react-query'
import { getRelicSetListApi } from '@/lib/api'
import { useEffect } from 'react'
import useDetailDataStore from '@/stores/detailDataStore'
import { toast } from 'react-toastify'
import { useShallow } from 'zustand/react/shallow'
export const useFetchRelicSetData = () => {
const { setMapRelicSet } = useDetailDataStore(
useShallow((state) => ({ setMapRelicSet: state.setMapRelicSet }))
)
const query = useQuery({
queryKey: ['RelicSetData'],
queryFn: getRelicSetListApi,
staleTime: 1000 * 60 * 5,
})
useEffect(() => {
if (query.data && !query.error) {
setMapRelicSet(query.data)
} else if (query.error) {
toast.error("Failed to load RelicSet data")
}
}, [query.data, query.error, setMapRelicSet])
return query
}
+24 -24
View File
@@ -2,19 +2,19 @@ import { create } from 'zustand'
import { persist } from 'zustand/middleware';
import { connectPersistedSchema } from '@/zod';
import { createValidatedJSONStorage } from './persistStorage';
interface ConnectState {
connectionType: string;
privateType: string;
serverUrl: string;
username: string;
password: string;
setConnectionType: (newConnectionType: string) => void;
setPrivateType: (newPrivateType: string) => void;
setServerUrl: (newServerUrl: string) => void;
setUsername: (newUsername: string) => void;
setPassword: (newPassword: string) => void;
connectionType: string;
privateType: string;
serverUrl: string;
username: string;
password: string;
setConnectionType: (newConnectionType: string) => void;
setPrivateType: (newPrivateType: string) => void;
setServerUrl: (newServerUrl: string) => void;
setUsername: (newUsername: string) => void;
setPassword: (newPassword: string) => void;
}
type ConnectPersistedState = Pick<ConnectState, "connectionType" | "privateType" | "serverUrl" | "username" | "password">;
@@ -22,17 +22,17 @@ type ConnectPersistedState = Pick<ConnectState, "connectionType" | "privateType"
const useConnectStore = create<ConnectState>()(
persist<ConnectState, [], [], ConnectPersistedState>(
(set) => ({
connectionType: "FireflyGo",
privateType: "Local",
serverUrl: "http://localhost:21000",
username: "",
password: "",
setConnectionType: (newConnectionType: string) => set({ connectionType: newConnectionType }),
setPrivateType: (newPrivateType: string) => set({ privateType: newPrivateType }),
setServerUrl: (newServerUrl: string) => set({ serverUrl: newServerUrl }),
setUsername: (newUsername: string) => set({ username: newUsername }),
setPassword: (newPassword: string) => set({ password: newPassword }),
}),
connectionType: "FireflyGo",
privateType: "Local",
serverUrl: "http://localhost:21000",
username: "",
password: "",
setConnectionType: (newConnectionType: string) => set({ connectionType: newConnectionType }),
setPrivateType: (newPrivateType: string) => set({ privateType: newPrivateType }),
setServerUrl: (newServerUrl: string) => set({ serverUrl: newServerUrl }),
setUsername: (newUsername: string) => set({ username: newUsername }),
setPassword: (newPassword: string) => set({ password: newPassword }),
}),
{
name: 'connect-storage',
storage: createValidatedJSONStorage(() => localStorage, connectPersistedSchema),
@@ -46,5 +46,5 @@ const useConnectStore = create<ConnectState>()(
}
)
);
export default useConnectStore;
+38 -40
View File
@@ -1,41 +1,39 @@
import { AvatarDetail, AvatarProfileCardType, BaseTypeData, DamageTypeData } from '@/types';
import { create } from 'zustand'
interface CopyProfileState {
selectedProfiles: AvatarProfileCardType[];
avatarCopySelected: AvatarDetail | null;
listElement: Record<string, boolean>;
listPath: Record<string, boolean>;
listRank: Record<string, boolean>;
setListElement: (newListElement: Record<string, boolean>) => void;
setListPath: (newListPath: Record<string, boolean>) => void;
setListRank: (newListRank: Record<string, boolean>) => void;
setSelectedProfiles: (newListAvatar: AvatarProfileCardType[]) => void;
setAvatarCopySelected: (newAvatarSelected: AvatarDetail | null) => void;
setResetData: (baseType: Record<string, BaseTypeData>, damageType: Record<string, DamageTypeData>) => void;
}
const useCopyProfileStore = create<CopyProfileState>((set) => ({
selectedProfiles: [],
avatarCopySelected: null,
listElement: {},
listPath: {},
listRank: { "4": false, "5": false },
listCopyAvatar: [],
listRawCopyAvatar: [],
setListElement: (newListElement: Record<string, boolean>) => set({ listElement: newListElement }),
setListPath: (newListPath: Record<string, boolean>) => set({ listPath: newListPath }),
setListRank: (newListRank: Record<string, boolean>) => set({ listRank: newListRank }),
setAvatarCopySelected: (newAvatarSelected: AvatarDetail | null) => set({ avatarCopySelected: newAvatarSelected }),
setSelectedProfiles: (newListAvatar: AvatarProfileCardType[]) => set({ selectedProfiles: newListAvatar }),
setResetData: (baseType: Record<string, BaseTypeData>, damageType: Record<string, DamageTypeData>) => {
set({
listElement: Object.fromEntries(Object.keys(damageType).map(key => [key, false])) as Record<string, boolean>,
listPath: Object.fromEntries(Object.keys(baseType).map(key => [key, false])) as Record<string, boolean>,
listRank: { "4": false, "5": false }
})
}
}));
import { AvatarDetail, AvatarProfileCardType, BaseTypeData, DamageTypeData } from '@/types';
import { create } from 'zustand'
interface CopyProfileState {
selectedProfiles: AvatarProfileCardType[];
avatarCopySelected: AvatarDetail | null;
listElement: Record<string, boolean>;
listPath: Record<string, boolean>;
listRank: Record<string, boolean>;
setListElement: (newListElement: Record<string, boolean>) => void;
setListPath: (newListPath: Record<string, boolean>) => void;
setListRank: (newListRank: Record<string, boolean>) => void;
setSelectedProfiles: (newListAvatar: AvatarProfileCardType[]) => void;
setAvatarCopySelected: (newAvatarSelected: AvatarDetail | null) => void;
setResetData: (baseType: Record<string, BaseTypeData>, damageType: Record<string, DamageTypeData>) => void;
}
const useCopyProfileStore = create<CopyProfileState>((set) => ({
selectedProfiles: [],
avatarCopySelected: null,
listElement: {},
listPath: {},
listRank: { "4": false, "5": false },
setListElement: (newListElement: Record<string, boolean>) => set({ listElement: newListElement }),
setListPath: (newListPath: Record<string, boolean>) => set({ listPath: newListPath }),
setListRank: (newListRank: Record<string, boolean>) => set({ listRank: newListRank }),
setAvatarCopySelected: (newAvatarSelected: AvatarDetail | null) => set({ avatarCopySelected: newAvatarSelected }),
setSelectedProfiles: (newListAvatar: AvatarProfileCardType[]) => set({ selectedProfiles: newListAvatar }),
setResetData: (baseType: Record<string, BaseTypeData>, damageType: Record<string, DamageTypeData>) => {
set({
listElement: Object.fromEntries(Object.keys(damageType).map(key => [key, false])) as Record<string, boolean>,
listPath: Object.fromEntries(Object.keys(baseType).map(key => [key, false])) as Record<string, boolean>,
listRank: { "4": false, "5": false }
})
}
}));
export default useCopyProfileStore;
+59 -57
View File
@@ -1,58 +1,60 @@
import { AvatarDetail, BaseTypeData, Metadata } from '@/types'
import { create } from 'zustand'
interface CurrentDataState {
avatarSelected: AvatarDetail | null;
skillIDSelected: string | null;
avatarSearch: string;
lightconeSearch: string;
mapAvatarElementActive: Record<string, boolean>;
mapAvatarPathActive: Record<string, boolean>;
mapLightconePathActive: Record<string, boolean>;
mapLightconeRankActive: Record<string, boolean>;
setSettingData: (newMetaData: Metadata) => void;
setAvatarSelected: (avatar: AvatarDetail | null) => void;
setSkillIDSelected: (skillID: string | null) => void;
setAvatarSearch: (avatarSearch: string) => void;
setLightconeSearch: (lightconeSearch: string) => void;
setMapAvatarElementActive: (mapAvatarElementActive: Record<string, boolean>) => void;
setMapAvatarPathActive: (mapAvatarPathActive: Record<string, boolean>) => void;
setMapLightconePathActive: (mapLightconePathActive: Record<string, boolean>) => void;
setMapLightconeRankActive: (mapLightconeRankActive: Record<string, boolean>) => void;
setResetDataLightcone: (avtarSeleted: AvatarDetail| null, baseType: Record<string, BaseTypeData>) => void;
}
const useCurrentDataStore = create<CurrentDataState>((set) => ({
avatarSelected: null,
skillIDSelected: null,
mapAvatarElementActive: {},
mapAvatarPathActive: {},
mapLightconePathActive: {},
mapLightconeRankActive: {"3": false, "4": false, "5": false },
avatarSearch: "",
lightconeSearch: "",
setSettingData: (newMetaData: Metadata) => set({
mapAvatarElementActive: Object.fromEntries(Object.keys(newMetaData.DamageType).map(key => [key, false])) as Record<string, boolean>,
mapAvatarPathActive: Object.fromEntries(Object.keys(newMetaData.BaseType).map(key => [key, false])) as Record<string, boolean>,
mapLightconePathActive: Object.fromEntries(Object.keys(newMetaData.BaseType).map(key => [key, false])) as Record<string, boolean>,
}),
setAvatarSearch: (avatarSearch: string) => set({ avatarSearch }),
setLightconeSearch: (lightconeSearch: string) => set({ lightconeSearch }),
setAvatarSelected: (avatar: AvatarDetail | null) => set({ avatarSelected: avatar }),
setSkillIDSelected: (skillID: string | null) => set({ skillIDSelected: skillID }),
setMapAvatarElementActive: (mapAvatarElementActive: Record<string, boolean>) => set({ mapAvatarElementActive }),
setMapAvatarPathActive: (mapAvatarPathActive: Record<string, boolean>) => set({ mapAvatarPathActive }),
setMapLightconePathActive: (mapLightconePathActive: Record<string, boolean>) => set({ mapLightconePathActive }),
setMapLightconeRankActive: (mapLightconeRankActive: Record<string, boolean>) => set({ mapLightconeRankActive }),
setResetDataLightcone: (avtarSeleted: AvatarDetail| null, baseType: Record<string, BaseTypeData>) => {
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 }
if (avtarSeleted) {
newListPath[avtarSeleted.BaseType] = true
}
set({ mapLightconeRankActive: newListRank })
set({ mapLightconePathActive: newListPath })
}
}))
import { AvatarDetail, BaseTypeData, Metadata } from '@/types'
import { create } from 'zustand'
interface CurrentDataState {
avatarSelected: AvatarDetail | null;
skillIDSelected: string | null;
avatarSearch: string;
lightconeSearch: string;
mapAvatarElementActive: Record<string, boolean>;
mapAvatarPathActive: Record<string, boolean>;
mapLightconePathActive: Record<string, boolean>;
mapLightconeRankActive: Record<string, boolean>;
setSettingData: (newMetaData: Metadata) => void;
setAvatarSelected: (avatar: AvatarDetail | null) => void;
setSkillIDSelected: (skillID: string | null) => void;
setAvatarSearch: (avatarSearch: string) => void;
setLightconeSearch: (lightconeSearch: string) => void;
setMapAvatarElementActive: (mapAvatarElementActive: Record<string, boolean>) => void;
setMapAvatarPathActive: (mapAvatarPathActive: Record<string, boolean>) => void;
setMapLightconePathActive: (mapLightconePathActive: Record<string, boolean>) => void;
setMapLightconeRankActive: (mapLightconeRankActive: Record<string, boolean>) => void;
setResetDataLightcone: (avatarSelected: AvatarDetail | null, baseType: Record<string, BaseTypeData>) => void;
}
const useCurrentDataStore = create<CurrentDataState>((set) => ({
avatarSelected: null,
skillIDSelected: null,
mapAvatarElementActive: {},
mapAvatarPathActive: {},
mapLightconePathActive: {},
mapLightconeRankActive: {"3": false, "4": false, "5": false },
avatarSearch: "",
lightconeSearch: "",
setSettingData: (newMetaData: Metadata) => set({
mapAvatarElementActive: Object.fromEntries(Object.keys(newMetaData.DamageType).map(key => [key, false])) as Record<string, boolean>,
mapAvatarPathActive: Object.fromEntries(Object.keys(newMetaData.BaseType).map(key => [key, false])) as Record<string, boolean>,
mapLightconePathActive: Object.fromEntries(Object.keys(newMetaData.BaseType).map(key => [key, false])) as Record<string, boolean>,
}),
setAvatarSearch: (avatarSearch: string) => set({ avatarSearch }),
setLightconeSearch: (lightconeSearch: string) => set({ lightconeSearch }),
setAvatarSelected: (avatar: AvatarDetail | null) => set({ avatarSelected: avatar }),
setSkillIDSelected: (skillID: string | null) => set({ skillIDSelected: skillID }),
setMapAvatarElementActive: (mapAvatarElementActive: Record<string, boolean>) => set({ mapAvatarElementActive }),
setMapAvatarPathActive: (mapAvatarPathActive: Record<string, boolean>) => set({ mapAvatarPathActive }),
setMapLightconePathActive: (mapLightconePathActive: Record<string, boolean>) => set({ mapLightconePathActive }),
setMapLightconeRankActive: (mapLightconeRankActive: Record<string, boolean>) => set({ mapLightconeRankActive }),
setResetDataLightcone: (avatarSelected: AvatarDetail | null, baseType: Record<string, BaseTypeData>) => {
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 }
if (avatarSelected) {
newListPath[avatarSelected.BaseType] = true
}
set({
mapLightconeRankActive: newListRank,
mapLightconePathActive: newListPath,
})
}
}))
export default useCurrentDataStore;
+69 -69
View File
@@ -1,69 +1,69 @@
import { ASGroupDetail, AvatarDetail, BaseTypeData, DamageTypeData, EliteData, HardLevelData, LightConeDetail, MainAffixData, Metadata, MOCGroupDetail, MonsterDetail, PeakGroupDetail, PFGroupDetail, RelicSetDetail, SkillMaxLevelData, StageData, SubAffixData } from '@/types'
import { create } from 'zustand'
interface DetailDataState {
mapMonster: Record<string, MonsterDetail>
mapRelicSet: Record<string, RelicSetDetail>
mapAvatar: Record<string, AvatarDetail>
mapLightCone: Record<string, LightConeDetail>
mapAS: Record<string, ASGroupDetail>
mapMoc: Record<string, MOCGroupDetail>
mapPF: Record<string, PFGroupDetail>
mapPeak: Record<string, PeakGroupDetail>
baseType: Record<string, BaseTypeData>;
damageType: Record<string, DamageTypeData>;
mainAffix: Record<string, Record<string, MainAffixData>>;
subAffix: Record<string, Record<string, SubAffixData>>;
skillConfig: Record<string, SkillMaxLevelData>;
stage: Record<string, StageData>;
hardLevelConfig: Record<string, Record<string, HardLevelData>>;
eliteConfig: Record<string, EliteData>;
setMetaData: (newMetaData: Metadata) => void;
setMapMonster: (newMonster: Record<string, MonsterDetail>) => void
setMapRelicSet: (newRelicSet: Record<string, RelicSetDetail>) => void
setMapAvatar: (newAvatar: Record<string, AvatarDetail>) => void
setMapLightCone: (newLightCone: Record<string, LightConeDetail>) => void
setMapAS: (newAS: Record<string, ASGroupDetail>) => void
setMapMoc: (newMoc: Record<string, MOCGroupDetail>) => void
setMapPF: (newPF: Record<string, PFGroupDetail>) => void
setMapPeak: (newPeak: Record<string, PeakGroupDetail>) => void
}
const useDetailDataStore = create<DetailDataState>((set) => ({
mapMonster: {},
mapRelicSet: {},
mapAvatar: {},
mapLightCone: {},
mapAS: {},
mapMoc: {},
mapPF: {},
mapPeak: {},
baseType: {},
damageType: {},
mainAffix: {},
subAffix: {},
skillConfig: {},
stage: {},
hardLevelConfig: {},
eliteConfig: {},
setMapMonster: (newMonster) => set({ mapMonster: newMonster }),
setMapRelicSet: (newRelicSet) => set({ mapRelicSet: newRelicSet }),
setMapAvatar: (newAvatar) => set({ mapAvatar: newAvatar }),
setMapLightCone: (newLightCone) => set({ mapLightCone: newLightCone }),
setMapAS: (newAS) => set({ mapAS: newAS }),
setMapMoc: (newMoc) => set({ mapMoc: newMoc }),
setMapPF: (newPF) => set({ mapPF: newPF }),
setMapPeak: (newPeak) => set({ mapPeak: newPeak }),
setMetaData: (newMetaData: Metadata) => set({
baseType: newMetaData.BaseType,
damageType: newMetaData.DamageType,
mainAffix: newMetaData.MainAffix,
subAffix: newMetaData.SubAffix,
skillConfig: newMetaData.SkillConfig,
stage: newMetaData.Stage,
hardLevelConfig: newMetaData.HardLevelConfig,
eliteConfig: newMetaData.EliteConfig,
}),
}))
export default useDetailDataStore
import { ASGroupDetail, AvatarDetail, BaseTypeData, DamageTypeData, EliteData, HardLevelData, LightConeDetail, MainAffixData, Metadata, MOCGroupDetail, MonsterDetail, PeakGroupDetail, PFGroupDetail, RelicSetDetail, SkillMaxLevelData, StageData, SubAffixData } from '@/types'
import { create } from 'zustand'
interface DetailDataState {
mapMonster: Record<string, MonsterDetail>
mapRelicSet: Record<string, RelicSetDetail>
mapAvatar: Record<string, AvatarDetail>
mapLightCone: Record<string, LightConeDetail>
mapAS: Record<string, ASGroupDetail>
mapMoc: Record<string, MOCGroupDetail>
mapPF: Record<string, PFGroupDetail>
mapPeak: Record<string, PeakGroupDetail>
baseType: Record<string, BaseTypeData>;
damageType: Record<string, DamageTypeData>;
mainAffix: Record<string, Record<string, MainAffixData>>;
subAffix: Record<string, Record<string, SubAffixData>>;
skillConfig: Record<string, SkillMaxLevelData>;
stage: Record<string, StageData>;
hardLevelConfig: Record<string, Record<string, HardLevelData>>;
eliteConfig: Record<string, EliteData>;
setMetaData: (newMetaData: Metadata) => void;
setMapMonster: (newMonster: Record<string, MonsterDetail>) => void
setMapRelicSet: (newRelicSet: Record<string, RelicSetDetail>) => void
setMapAvatar: (newAvatar: Record<string, AvatarDetail>) => void
setMapLightCone: (newLightCone: Record<string, LightConeDetail>) => void
setMapAS: (newAS: Record<string, ASGroupDetail>) => void
setMapMoc: (newMoc: Record<string, MOCGroupDetail>) => void
setMapPF: (newPF: Record<string, PFGroupDetail>) => void
setMapPeak: (newPeak: Record<string, PeakGroupDetail>) => void
}
const useDetailDataStore = create<DetailDataState>((set) => ({
mapMonster: {},
mapRelicSet: {},
mapAvatar: {},
mapLightCone: {},
mapAS: {},
mapMoc: {},
mapPF: {},
mapPeak: {},
baseType: {},
damageType: {},
mainAffix: {},
subAffix: {},
skillConfig: {},
stage: {},
hardLevelConfig: {},
eliteConfig: {},
setMapMonster: (newMonster) => set({ mapMonster: newMonster }),
setMapRelicSet: (newRelicSet) => set({ mapRelicSet: newRelicSet }),
setMapAvatar: (newAvatar) => set({ mapAvatar: newAvatar }),
setMapLightCone: (newLightCone) => set({ mapLightCone: newLightCone }),
setMapAS: (newAS) => set({ mapAS: newAS }),
setMapMoc: (newMoc) => set({ mapMoc: newMoc }),
setMapPF: (newPF) => set({ mapPF: newPF }),
setMapPeak: (newPeak) => set({ mapPeak: newPeak }),
setMetaData: (newMetaData: Metadata) => set({
baseType: newMetaData.BaseType,
damageType: newMetaData.DamageType,
mainAffix: newMetaData.MainAffix,
subAffix: newMetaData.SubAffix,
skillConfig: newMetaData.SkillConfig,
stage: newMetaData.Stage,
hardLevelConfig: newMetaData.HardLevelConfig,
eliteConfig: newMetaData.EliteConfig,
}),
}))
export default useDetailDataStore
+16 -16
View File
@@ -1,22 +1,22 @@
import { CharacterInfoCardType, EnkaResponse } from '@/types';
import { create } from 'zustand'
interface EnkaState {
selectedCharacters: CharacterInfoCardType[];
enkaData: EnkaResponse | null;
import { CharacterInfoCardType, EnkaResponse } from '@/types';
import { create } from 'zustand'
interface EnkaState {
selectedCharacters: CharacterInfoCardType[];
enkaData: EnkaResponse | null;
uidInput: string;
setSelectedCharacters: (newListAvatar: CharacterInfoCardType[]) => void;
setEnkaData: (newEnkaData: EnkaResponse | null) => void;
setUidInput: (newUidInput: string) => void;
}
const useEnkaStore = create<EnkaState>((set) => ({
selectedCharacters: [],
enkaData: null,
uidInput: "",
setUidInput: (newUidInput: string) => set({ uidInput: newUidInput }),
setSelectedCharacters: (newListAvatar: CharacterInfoCardType[]) => set({ selectedCharacters: newListAvatar }),
setUidInput: (newUidInput: string) => void;
}
const useEnkaStore = create<EnkaState>((set) => ({
selectedCharacters: [],
enkaData: null,
uidInput: "",
setUidInput: (newUidInput: string) => set({ uidInput: newUidInput }),
setSelectedCharacters: (newListAvatar: CharacterInfoCardType[]) => set({ selectedCharacters: newListAvatar }),
setEnkaData: (newEnkaData: EnkaResponse | null) => set({ enkaData: newEnkaData }),
}));
export default useEnkaStore;
+17 -17
View File
@@ -1,18 +1,18 @@
import { CharacterInfoCardType, FreeSRJson } from '@/types';
import { create } from 'zustand'
interface FreeSRState {
selectedCharacters: CharacterInfoCardType[];
freeSRData: FreeSRJson | null;
setSelectedCharacters: (newListAvatar: CharacterInfoCardType[]) => void;
setFreeSRData: (newFreeSRData: FreeSRJson | null) => void;
}
const useFreeSRStore = create<FreeSRState>((set) => ({
selectedCharacters: [],
freeSRData: null,
setSelectedCharacters: (newListAvatar: CharacterInfoCardType[]) => set({ selectedCharacters: newListAvatar }),
setFreeSRData: (newFreeSRData: FreeSRJson | null) => set({ freeSRData: newFreeSRData }),
}));
import { CharacterInfoCardType, FreeSRJson } from '@/types';
import { create } from 'zustand'
interface FreeSRState {
selectedCharacters: CharacterInfoCardType[];
freeSRData: FreeSRJson | null;
setSelectedCharacters: (newListAvatar: CharacterInfoCardType[]) => void;
setFreeSRData: (newFreeSRData: FreeSRJson | null) => void;
}
const useFreeSRStore = create<FreeSRState>((set) => ({
selectedCharacters: [],
freeSRData: null,
setSelectedCharacters: (newListAvatar: CharacterInfoCardType[]) => set({ selectedCharacters: newListAvatar }),
setFreeSRData: (newFreeSRData: FreeSRJson | null) => set({ freeSRData: newFreeSRData }),
}));
export default useFreeSRStore;
+25 -25
View File
@@ -1,26 +1,26 @@
import { create } from 'zustand'
import { ExtraData } from '@/types'
interface GlobalState {
isConnectPS: boolean;
extraData?: ExtraData;
isEnableChangePath: boolean
isEnableLua: boolean
setIsEnableChangePath: (newIsEnableChangePath: boolean) => void;
setIsEnableLua: (newIsEnableLua: boolean) => void;
setExtraData: (newExtraData: ExtraData | undefined) => void;
setIsConnectPS: (newIsConnectPS: boolean) => void;
}
const useGlobalStore = create<GlobalState>((set) => ({
isConnectPS: false,
extraData: undefined,
isEnableChangePath: false,
isEnableLua: false,
setIsEnableChangePath: (newIsEnableChangePath: boolean) => set({ isEnableChangePath: newIsEnableChangePath }),
setIsEnableLua: (newIsEnableLua: boolean) => set({ isEnableLua: newIsEnableLua }),
setExtraData: (newExtraData: ExtraData | undefined) => set({ extraData: newExtraData }),
setIsConnectPS: (newIsConnectPS: boolean) => set({ isConnectPS: newIsConnectPS }),
}));
import { create } from 'zustand'
import { ExtraData } from '@/types'
interface GlobalState {
isConnectPS: boolean;
extraData?: ExtraData;
isEnableChangePath: boolean
isEnableLua: boolean
setIsEnableChangePath: (newIsEnableChangePath: boolean) => void;
setIsEnableLua: (newIsEnableLua: boolean) => void;
setExtraData: (newExtraData: ExtraData | undefined) => void;
setIsConnectPS: (newIsConnectPS: boolean) => void;
}
const useGlobalStore = create<GlobalState>((set) => ({
isConnectPS: false,
extraData: undefined,
isEnableChangePath: false,
isEnableLua: false,
setIsEnableChangePath: (newIsEnableChangePath: boolean) => set({ isEnableChangePath: newIsEnableChangePath }),
setIsEnableLua: (newIsEnableLua: boolean) => set({ isEnableLua: newIsEnableLua }),
setExtraData: (newExtraData: ExtraData | undefined) => set({ extraData: newExtraData }),
setIsConnectPS: (newIsConnectPS: boolean) => set({ isConnectPS: newIsConnectPS }),
}));
export default useGlobalStore;
+20 -20
View File
@@ -3,17 +3,17 @@ import { create } from 'zustand'
import { persist } from 'zustand/middleware';
import { localePersistedSchema } from '@/zod';
import { createValidatedJSONStorage } from './persistStorage';
interface LocaleState {
locale: string;
theme: string;
currentVersion: string
changelog: ChangelogItemType[]
setCurrentVersion: (currentVersion: string) => void;
setChangelog: (newChangelog: ChangelogItemType[]) => void;
setTheme: (newTheme: string) => void;
setLocale: (newLocale: string) => void;
locale: string;
theme: string;
currentVersion: string
changelog: ChangelogItemType[]
setCurrentVersion: (currentVersion: string) => void;
setChangelog: (newChangelog: ChangelogItemType[]) => void;
setTheme: (newTheme: string) => void;
setLocale: (newLocale: string) => void;
}
type LocalePersistedState = Pick<LocaleState, "locale" | "theme" | "currentVersion" | "changelog">;
@@ -21,15 +21,15 @@ type LocalePersistedState = Pick<LocaleState, "locale" | "theme" | "currentVersi
const useLocaleStore = create<LocaleState>()(
persist<LocaleState, [], [], LocalePersistedState>(
(set) => ({
locale: "en",
theme: "night",
currentVersion: "",
changelog: [],
setCurrentVersion: (currentVersion: string) => set({ currentVersion }),
setChangelog: (newChangelog: ChangelogItemType[]) => set({ changelog: newChangelog }),
setTheme: (newTheme: string) => set({ theme: newTheme }),
setLocale: (newLocale: string) => set({ locale: newLocale }),
}),
locale: "en",
theme: "night",
currentVersion: "",
changelog: [],
setCurrentVersion: (currentVersion: string) => set({ currentVersion }),
setChangelog: (newChangelog: ChangelogItemType[]) => set({ changelog: newChangelog }),
setTheme: (newTheme: string) => set({ theme: newTheme }),
setLocale: (newLocale: string) => set({ locale: newLocale }),
}),
{
name: 'locale-storage',
storage: createValidatedJSONStorage(() => localStorage, localePersistedSchema),
@@ -42,5 +42,5 @@ const useLocaleStore = create<LocaleState>()(
}
)
);
export default useLocaleStore;
+53 -57
View File
@@ -1,58 +1,54 @@
import { create } from 'zustand'
interface ModelState {
isOpenLightcone: boolean;
isOpenRelic: boolean;
isOpenAvatar: boolean;
isOpenCreateProfile: boolean;
isOpenImport: boolean;
isOpenCopy: boolean;
isOpenMonster: boolean;
isOpenConnect: boolean;
isOpenAvatars: boolean;
isOpenQuickView: boolean;
isOpenExtra: boolean;
isChangelog: boolean;
setIsOpenExtra: (newIsOpenExtra: boolean) => void;
setIsOpenQuickView: (newIsOpenQuickView: boolean) => void;
setIsOpenAvatars: (newIsOpenAvatars: boolean) => void;
setIsOpenConnect: (newIsOpenConnect: boolean) => void;
setIsOpenMonster: (newIsOpenMonster: boolean) => void;
setIsOpenLightcone: (newIsOpenLightcone: boolean) => void;
setIsOpenRelic: (newIsOpenRelic: boolean) => void;
setIsOpenAvatar: (newIsOpenAvatar: boolean) => void;
setIsOpenCreateProfile: (newIsOpenCreateProfile: boolean) => void;
setIsOpenImport: (newIsOpenImport: boolean) => void;
setIsOpenCopy: (newIsOpenCopy: boolean) => void;
setIsChangelog: (newChangelog: boolean) => void;
}
const useModelStore = create<ModelState>((set) => ({
isOpenLightcone: false,
isOpenRelic: false,
isOpenAvatar: false,
isOpenCreateProfile: false,
isOpenImport: false,
isOpenCopy: false,
isOpenMonster: false,
isOpenConnect: false,
isOpenAvatars: false,
isOpenQuickView: false,
isOpenExtra: false,
isChangelog: false,
setIsOpenExtra: (newIsOpenExtra: boolean) => set({ isOpenExtra: newIsOpenExtra }),
setIsOpenQuickView: (newIsOpenQuickView: boolean) => set({ isOpenQuickView: newIsOpenQuickView }),
setIsOpenAvatars: (newIsOpenAvatars: boolean) => set({ isOpenAvatars: newIsOpenAvatars }),
setIsOpenConnect: (newIsOpenConnect: boolean) => set({ isOpenConnect: newIsOpenConnect }),
setIsOpenMonster: (newIsOpenMonster: boolean) => set({ isOpenMonster: newIsOpenMonster }),
setIsOpenLightcone: (newIsOpenLightcone: boolean) => set({ isOpenLightcone: newIsOpenLightcone }),
setIsOpenRelic: (newIsOpenRelic: boolean) => set({ isOpenRelic: newIsOpenRelic }),
setIsOpenAvatar: (newIsOpenAvatar: boolean) => set({ isOpenAvatar: newIsOpenAvatar }),
setIsOpenCreateProfile: (newIsOpenCreateProfile: boolean) => set({ isOpenCreateProfile: newIsOpenCreateProfile }),
setIsOpenImport: (newIsOpenImport: boolean) => set({ isOpenImport: newIsOpenImport }),
setIsOpenCopy: (newIsOpenCopy: boolean) => set({ isOpenCopy: newIsOpenCopy }),
setIsChangelog: (newChangelog: boolean) => set({ isChangelog: newChangelog }),
}));
import { create } from 'zustand'
interface ModelState {
isOpenLightcone: boolean;
isOpenRelic: boolean;
isOpenCreateProfile: boolean;
isOpenImport: boolean;
isOpenCopy: boolean;
isOpenMonster: boolean;
isOpenConnect: boolean;
isOpenAvatars: boolean;
isOpenQuickView: boolean;
isOpenExtra: boolean;
isChangelog: boolean;
setIsOpenExtra: (newIsOpenExtra: boolean) => void;
setIsOpenQuickView: (newIsOpenQuickView: boolean) => void;
setIsOpenAvatars: (newIsOpenAvatars: boolean) => void;
setIsOpenConnect: (newIsOpenConnect: boolean) => void;
setIsOpenMonster: (newIsOpenMonster: boolean) => void;
setIsOpenLightcone: (newIsOpenLightcone: boolean) => void;
setIsOpenRelic: (newIsOpenRelic: boolean) => void;
setIsOpenCreateProfile: (newIsOpenCreateProfile: boolean) => void;
setIsOpenImport: (newIsOpenImport: boolean) => void;
setIsOpenCopy: (newIsOpenCopy: boolean) => void;
setIsChangelog: (newChangelog: boolean) => void;
}
const useModelStore = create<ModelState>((set) => ({
isOpenLightcone: false,
isOpenRelic: false,
isOpenCreateProfile: false,
isOpenImport: false,
isOpenCopy: false,
isOpenMonster: false,
isOpenConnect: false,
isOpenAvatars: false,
isOpenQuickView: false,
isOpenExtra: false,
isChangelog: false,
setIsOpenExtra: (newIsOpenExtra: boolean) => set({ isOpenExtra: newIsOpenExtra }),
setIsOpenQuickView: (newIsOpenQuickView: boolean) => set({ isOpenQuickView: newIsOpenQuickView }),
setIsOpenAvatars: (newIsOpenAvatars: boolean) => set({ isOpenAvatars: newIsOpenAvatars }),
setIsOpenConnect: (newIsOpenConnect: boolean) => set({ isOpenConnect: newIsOpenConnect }),
setIsOpenMonster: (newIsOpenMonster: boolean) => set({ isOpenMonster: newIsOpenMonster }),
setIsOpenLightcone: (newIsOpenLightcone: boolean) => set({ isOpenLightcone: newIsOpenLightcone }),
setIsOpenRelic: (newIsOpenRelic: boolean) => set({ isOpenRelic: newIsOpenRelic }),
setIsOpenCreateProfile: (newIsOpenCreateProfile: boolean) => set({ isOpenCreateProfile: newIsOpenCreateProfile }),
setIsOpenImport: (newIsOpenImport: boolean) => set({ isOpenImport: newIsOpenImport }),
setIsOpenCopy: (newIsOpenCopy: boolean) => set({ isOpenCopy: newIsOpenCopy }),
setIsChangelog: (newChangelog: boolean) => set({ isChangelog: newChangelog }),
}));
export default useModelStore;
+205 -212
View File
@@ -1,213 +1,206 @@
import { create } from 'zustand'
interface RelicMakerState {
selectedRelicSlot: string;
selectedMainStat: string;
selectedRelicSet: string;
selectedRelicLevel: number;
listSelectedSubStats: {
property: string,
affixId: string,
rollCount: number,
stepCount: number
}[];
preSelectedSubStats: Record<string, {
property: string,
affixId: string,
rollCount: number,
stepCount: number
}[]>;
setSelectedRelicSlot: (newSelectedRelicSlot: string) => void;
setSelectedRelicSet: (newSelectedRelicSet: string) => void;
setSelectedMainStat: (newSelectedMainStat: string) => void;
setSelectedRelicLevel: (newSelectedRelicLevel: number) => void;
setListSelectedSubStats: (newSubAffixes: { property: string, affixId: string, rollCount: number, stepCount: number }[]) => void;
resetHistory: (index: number | null) => void;
resetSubStat: () => void;
popHistory: (index: number) => void;
addHistory: (index: number, affix: { property: string, affixId: string, rollCount: number, stepCount: number }) => void;
}
const useRelicMakerStore = create<RelicMakerState>((set) => ({
selectedRelicSlot: "",
selectedMainStat: "",
selectedRelicSet: "",
selectedRelicLevel: 15,
listSelectedSubStats: [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
preSelectedSubStats: {
"0": [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
"1": [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
"2": [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
"3": [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
},
resetSubStat: () => set({
listSelectedSubStats: [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
}),
setSelectedRelicSlot: (newSelectedRelicSlot: string) => set({ selectedRelicSlot: newSelectedRelicSlot }),
setSelectedRelicSet: (newSelectedRelicSet: string) => set({ selectedRelicSet: newSelectedRelicSet }),
setSelectedMainStat: (newSelectedMainStat: string) => set({ selectedMainStat: newSelectedMainStat }),
setSelectedRelicLevel: (newSelectedRelicLevel: number) => set({ selectedRelicLevel: newSelectedRelicLevel }),
setListSelectedSubStats: (newSubAffixes) => set(() => ({
listSelectedSubStats: newSubAffixes.map(item => ({ ...item }))
})),
resetHistory: (index: number | null) => set((state) => {
if (index !== null) {
return {
preSelectedSubStats: {
"0": [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
"1": [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
"2": [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
"3": [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
}
}
} else if (index !== null) {
const newPreSelectedSubStats = { ...state.preSelectedSubStats };
newPreSelectedSubStats[index] = [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
]
return {
preSelectedSubStats: newPreSelectedSubStats
}
}
return {}
}),
popHistory: (index: number) => set((state) => {
const newPreSelectedSubStats = structuredClone(state.preSelectedSubStats);
const copied = state.preSelectedSubStats[index].map(item => ({ ...item }));
copied.pop();
newPreSelectedSubStats[index] = copied;
return { preSelectedSubStats: newPreSelectedSubStats };
}),
addHistory: (index, affix) => set((state) => {
const newPreSelectedSubStats = structuredClone(state.preSelectedSubStats);
const currentList = state.preSelectedSubStats[index];
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 },
];
if (copied) {
const newAffix = structuredClone(affix);
copied.push(newAffix);
newPreSelectedSubStats[index] = copied;
return { preSelectedSubStats: newPreSelectedSubStats };
}
return {}
}),
}));
import { create } from 'zustand'
interface RelicMakerState {
selectedRelicSlot: string;
selectedMainStat: string;
selectedRelicSet: string;
selectedRelicLevel: number;
listSelectedSubStats: {
property: string,
affixId: string,
rollCount: number,
stepCount: number
}[];
preSelectedSubStats: Record<string, {
property: string,
affixId: string,
rollCount: number,
stepCount: number
}[]>;
setSelectedRelicSlot: (newSelectedRelicSlot: string) => void;
setSelectedRelicSet: (newSelectedRelicSet: string) => void;
setSelectedMainStat: (newSelectedMainStat: string) => void;
setSelectedRelicLevel: (newSelectedRelicLevel: number) => void;
setListSelectedSubStats: (newSubAffixes: { property: string, affixId: string, rollCount: number, stepCount: number }[]) => void;
resetHistory: (index: number | null) => void;
resetSubStat: () => void;
popHistory: (index: number) => void;
addHistory: (index: number, affix: { property: string, affixId: string, rollCount: number, stepCount: number }) => void;
}
const useRelicMakerStore = create<RelicMakerState>((set) => ({
selectedRelicSlot: "",
selectedMainStat: "",
selectedRelicSet: "",
selectedRelicLevel: 15,
listSelectedSubStats: [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
preSelectedSubStats: {
"0": [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
"1": [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
"2": [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
"3": [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
},
resetSubStat: () => set({
listSelectedSubStats: [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
}),
setSelectedRelicSlot: (newSelectedRelicSlot: string) => set({ selectedRelicSlot: newSelectedRelicSlot }),
setSelectedRelicSet: (newSelectedRelicSet: string) => set({ selectedRelicSet: newSelectedRelicSet }),
setSelectedMainStat: (newSelectedMainStat: string) => set({ selectedMainStat: newSelectedMainStat }),
setSelectedRelicLevel: (newSelectedRelicLevel: number) => set({ selectedRelicLevel: newSelectedRelicLevel }),
setListSelectedSubStats: (newSubAffixes) => set(() => ({
listSelectedSubStats: newSubAffixes.map(item => ({ ...item }))
})),
resetHistory: (index: number | null) => set((state) => {
if (index === null) {
return {
preSelectedSubStats: {
"0": [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
"1": [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
"2": [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
"3": [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
],
}
}
}
const newPreSelectedSubStats = { ...state.preSelectedSubStats };
newPreSelectedSubStats[index.toString()] = [
{
property: "",
affixId: "",
rollCount: 0,
stepCount: 0
},
]
return {
preSelectedSubStats: newPreSelectedSubStats
}
}),
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 copied = currentList.map(item => ({ ...item }));
copied.pop();
newPreSelectedSubStats[key] = copied;
return { preSelectedSubStats: newPreSelectedSubStats };
}),
addHistory: (index, affix) => set((state) => {
const key = index.toString();
const newPreSelectedSubStats = structuredClone(state.preSelectedSubStats);
const currentList = state.preSelectedSubStats[key] || [
{ property: "", affixId: "", rollCount: 0, stepCount: 0 }
];
const copied = currentList.map(item => ({ ...item }));
copied.push(structuredClone(affix));
newPreSelectedSubStats[key] = copied;
return { preSelectedSubStats: newPreSelectedSubStats };
}),
}));
export default useRelicMakerStore;
+76 -76
View File
@@ -3,24 +3,24 @@ import { create } from 'zustand'
import { persist } from 'zustand/middleware';
import { micsSchema } from '@/zod';
import { createValidatedJSONStorage } from './persistStorage';
interface UserDataState {
avatars: { [key: string]: AvatarStore };
battle_type: string;
moc_config: MOCConfigStore;
pf_config: PFConfigStore;
as_config: ASConfigStore;
peak_config: PEAKConfigStore;
ce_config: CEConfigStore;
setAvatars: (newAvatars: { [key: string]: AvatarStore }) => void;
setAvatar: (newAvatar: AvatarStore) => void;
setBattleType: (newBattleType: string) => void;
setMocConfig: (newMocConfig: MOCConfigStore) => void;
setPfConfig: (newPfConfig: PFConfigStore) => void;
setAsConfig: (newAsConfig: ASConfigStore) => void;
setPeakConfig: (newPeakConfig: PEAKConfigStore) => void;
setCeConfig: (newCeConfig: CEConfigStore) => void;
avatars: { [key: string]: AvatarStore };
battle_type: string;
moc_config: MOCConfigStore;
pf_config: PFConfigStore;
as_config: ASConfigStore;
peak_config: PEAKConfigStore;
ce_config: CEConfigStore;
setAvatars: (newAvatars: { [key: string]: AvatarStore }) => void;
setAvatar: (newAvatar: AvatarStore) => void;
setBattleType: (newBattleType: string) => void;
setMocConfig: (newMocConfig: MOCConfigStore) => void;
setPfConfig: (newPfConfig: PFConfigStore) => void;
setAsConfig: (newAsConfig: ASConfigStore) => void;
setPeakConfig: (newPeakConfig: PEAKConfigStore) => void;
setCeConfig: (newCeConfig: CEConfigStore) => void;
}
type UserDataPersistedState = Pick<
@@ -37,64 +37,64 @@ type UserDataPersistedState = Pick<
const useUserDataStore = create<UserDataState>()(
persist<UserDataState, [], [], UserDataPersistedState>(
(set) => ({
avatars: {},
battle_type: "",
moc_config: {
event_id: 0,
challenge_id: 0,
floor_side: "Upper",
use_turbulence_buff: true,
use_cycle_count: true,
blessings: [],
cycle_count: 0,
stage_id: 0,
monsters: [],
},
pf_config: {
event_id: 0,
challenge_id: 0,
buff_id: 0,
floor_side: "Upper",
blessings: [],
cycle_count: 0,
stage_id: 0,
monsters: [],
},
as_config: {
event_id: 0,
challenge_id: 0,
buff_id: 0,
floor_side: "Upper",
blessings: [],
cycle_count: 0,
stage_id: 0,
monsters: [],
},
ce_config: {
blessings: [],
cycle_count: 30,
stage_id: 0,
monsters: [],
},
peak_config: {
event_id: 0,
challenge_id: 0,
buff_id: 0,
boss_mode: "Normal",
blessings: [],
cycle_count: 0,
stage_id: 0,
monsters: [],
},
setAvatars: (newAvatars: { [key: string]: AvatarStore }) => set({ avatars: newAvatars }),
setAvatar: (newAvatar: AvatarStore) => set((state) => ({ avatars: { ...state.avatars, [newAvatar.avatar_id.toString()]: newAvatar } })),
setBattleType: (newBattleType: string) => set({ battle_type: newBattleType }),
setMocConfig: (newMocConfig: MOCConfigStore) => set({ moc_config: newMocConfig }),
setPfConfig: (newPfConfig: PFConfigStore) => set({ pf_config: newPfConfig }),
setAsConfig: (newAsConfig: ASConfigStore) => set({ as_config: newAsConfig }),
setPeakConfig: (newPeakConfig: PEAKConfigStore) => set({ peak_config: newPeakConfig }),
setCeConfig: (newCeConfig: CEConfigStore) => set({ ce_config: newCeConfig }),
}),
avatars: {},
battle_type: "",
moc_config: {
event_id: 0,
challenge_id: 0,
floor_side: "firstNode",
use_turbulence_buff: true,
use_cycle_count: true,
blessings: [],
cycle_count: 0,
stage_id: 0,
monsters: [],
},
pf_config: {
event_id: 0,
challenge_id: 0,
buff_id: 0,
floor_side: "firstNode",
blessings: [],
cycle_count: 0,
stage_id: 0,
monsters: [],
},
as_config: {
event_id: 0,
challenge_id: 0,
buff_id: 0,
floor_side: "firstNode",
blessings: [],
cycle_count: 0,
stage_id: 0,
monsters: [],
},
ce_config: {
blessings: [],
cycle_count: 30,
stage_id: 0,
monsters: [],
},
peak_config: {
event_id: 0,
challenge_id: 0,
buff_id: 0,
boss_mode: "Normal",
blessings: [],
cycle_count: 0,
stage_id: 0,
monsters: [],
},
setAvatars: (newAvatars: { [key: string]: AvatarStore }) => set({ avatars: newAvatars }),
setAvatar: (newAvatar: AvatarStore) => set((state) => ({ avatars: { ...state.avatars, [newAvatar.avatar_id.toString()]: newAvatar } })),
setBattleType: (newBattleType: string) => set({ battle_type: newBattleType }),
setMocConfig: (newMocConfig: MOCConfigStore) => set({ moc_config: newMocConfig }),
setPfConfig: (newPfConfig: PFConfigStore) => set({ pf_config: newPfConfig }),
setAsConfig: (newAsConfig: ASConfigStore) => set({ as_config: newAsConfig }),
setPeakConfig: (newPeakConfig: PEAKConfigStore) => set({ peak_config: newPeakConfig }),
setCeConfig: (newCeConfig: CEConfigStore) => set({ ce_config: newCeConfig }),
}),
{
name: 'user-data-storage',
storage: createValidatedJSONStorage(() => localStorage, micsSchema),
@@ -110,5 +110,5 @@ const useUserDataStore = create<UserDataState>()(
}
)
);
export default useUserDataStore;
+129 -128
View File
@@ -1,129 +1,130 @@
import { ExtraEffect } from "./avatarDetail";
import { InfiniteWave, MazeBuff } from "./pfDetail";
export interface ASGroupDetail {
ID: number;
ChallengeGroupType: string;
Name: Record<string, string>;
BeginTime: string;
EndTime: string;
BuffList1: ASBuff[];
BuffList2: ASBuff[];
BuffList3: ASBuff[] | null;
Level: ASLevel[];
Tierce: ASTierceLevel | null;
}
export interface ASBuff {
ID: number;
Param: number[];
Icon: string;
Name: Record<string, string>;
Desc: Record<string, string>;
ExtraList?: ExtraEffect[];
}
export interface ASTierceLevel {
ID: number;
PreChallenge: number;
Name: Record<string, string>;
Target: ASTarget[];
DamageType: string[];
TurnLimit: number;
EventList: ASEvent[];
}
export interface ASLevel {
Floor: number;
ID: number;
StageNum: number;
Name: Record<string, string>;
Target: ASTarget[];
DamageType1: string[];
DamageType2: string[];
MazeBuff: MazeBuff[];
TurnLimit: number;
EventList1: ASEvent[];
EventList2: ASEvent[];
Monster1: ASMonster;
Monster2: ASMonster;
Monster3: ASMonster | null;
}
export interface ASTarget {
ID: number;
Name: Record<string, string>;
Param: number[];
}
export interface ASEvent {
ID: number;
Name: Record<string, string>;
HardLevelGroup: number;
EliteGroup: number;
Level: number;
Release: boolean;
MonsterList: number[][];
Infinite: InfiniteWave[] | null;
}
export interface ASMonster {
ID: number;
Tag: ASTag[];
Phase: ASPhase[];
DifficultyGuide: ASDifficultyGuide[];
TextGuide: ASTextGuide[];
}
export interface ASTag {
ID: number;
Name: Record<string, string>;
BriefDescription: Record<string, string>;
Param: number[];
SkillID: number | null;
Effect: ASEffect[];
}
export interface ASEffect {
ID: number;
Desc: Record<string, string>;
Name: Record<string, string>;
Icon: string;
Param: number[];
EffectType: number;
}
export interface ASPhase {
ID: number;
Name: Record<string, string>;
Answer: Record<string, string>;
Description: Record<string, string>;
SkillList: ASSkillList[];
}
export interface ASSkillList {
ID: number;
SkillType: string;
Name: Record<string, string>;
TextList: ASTextList[];
}
export interface ASTextList {
ID: number;
Description: Record<string, string>;
Param: number[];
Effect: ASEffect[];
}
export interface ASDifficultyGuide {
ID: number;
Description: Record<string, string>;
Param: number[];
SkillID: number | null;
}
export interface ASTextGuide {
ID: number;
Description: Record<string, string>;
Param: number[];
import { ExtraEffect } from "./avatarDetail";
import { InfiniteWave, Invasion, MazeBuff } from "./pfDetail";
export interface ASGroupDetail {
ID: number;
ChallengeGroupType: string;
Name: Record<string, string>;
BeginTime: string;
EndTime: string;
BuffList1: ASBuff[];
BuffList2: ASBuff[];
BuffList3: ASBuff[] | null;
Level: ASLevel[];
Tierce: ASTierceLevel | null;
}
export interface ASBuff {
ID: number;
Param: number[];
Icon: string;
Name: Record<string, string>;
Desc: Record<string, string>;
ExtraList?: ExtraEffect[];
}
export interface ASTierceLevel {
ID: number;
PreChallenge: number;
Name: Record<string, string>;
Target: ASTarget[];
DamageType: string[];
TurnLimit: number;
EventList: ASEvent[];
}
export interface ASLevel {
Floor: number;
ID: number;
StageNum: number;
Name: Record<string, string>;
Target: ASTarget[];
DamageType1: string[];
DamageType2: string[];
MazeBuff: MazeBuff[];
TurnLimit: number;
EventList1: ASEvent[];
EventList2: ASEvent[];
Monster1: ASMonster;
Monster2: ASMonster;
Monster3: ASMonster | null;
}
export interface ASTarget {
ID: number;
Name: Record<string, string>;
Param: number[];
}
export interface ASEvent {
ID: number;
Name: Record<string, string>;
HardLevelGroup: number;
EliteGroup: number;
Level: number;
Release: boolean;
MonsterList: number[][];
Invasion: Invasion | null;
Infinite: InfiniteWave[] | null;
}
export interface ASMonster {
ID: number;
Tag: ASTag[];
Phase: ASPhase[];
DifficultyGuide: ASDifficultyGuide[];
TextGuide: ASTextGuide[];
}
export interface ASTag {
ID: number;
Name: Record<string, string>;
BriefDescription: Record<string, string>;
Param: number[];
SkillID: number | null;
Effect: ASEffect[];
}
export interface ASEffect {
ID: number;
Desc: Record<string, string>;
Name: Record<string, string>;
Icon: string;
Param: number[];
EffectType: number;
}
export interface ASPhase {
ID: number;
Name: Record<string, string>;
Answer: Record<string, string>;
Description: Record<string, string>;
SkillList: ASSkillList[];
}
export interface ASSkillList {
ID: number;
SkillType: string;
Name: Record<string, string>;
TextList: ASTextList[];
}
export interface ASTextList {
ID: number;
Description: Record<string, string>;
Param: number[];
Effect: ASEffect[];
}
export interface ASDifficultyGuide {
ID: number;
Description: Record<string, string>;
Param: number[];
SkillID: number | null;
}
export interface ASTextGuide {
ID: number;
Description: Record<string, string>;
Param: number[];
}
+24 -24
View File
@@ -1,25 +1,25 @@
import { LightconeStore, RelicStore } from "./mics";
export interface CharacterInfoCardType {
key: number;
avatar_id: number;
rank: number;
level: number;
lightcone: {
level: number;
rank: number;
item_id: number;
};
relics: {
level: number;
relic_id: number;
relic_set_id: number;
}[];
}
export interface AvatarProfileCardType {
key: number;
profile_name: string,
lightcone?: LightconeStore | null,
relics: Record<string, RelicStore>
import { LightconeStore, RelicStore } from "./mics";
export interface CharacterInfoCardType {
key: number;
avatar_id: number;
rank: number;
level: number;
lightcone: {
level: number;
rank: number;
item_id: number;
};
relics: {
level: number;
relic_id: number;
relic_set_id: number;
}[];
}
export interface AvatarProfileCardType {
key: number;
profile_name: string,
lightcone?: LightconeStore | null,
relics: Record<string, RelicStore>
}
+6 -6
View File
@@ -1,7 +1,7 @@
export interface ChangelogItemType {
version: string,
date: string,
type: string,
items: string[]
export interface ChangelogItemType {
version: string,
date: string,
type: string,
items: string[]
}
+87 -87
View File
@@ -1,96 +1,96 @@
interface PrivacySettingInfo {
displayCollection: boolean
displayRecord: boolean
displayRecordTeam: boolean
displayOnlineStatus: boolean
displayDiary: boolean
}
interface RecordInfo {
achievementCount: number
bookCount: number
avatarCount: number
equipmentCount: number
musicCount: number
relicCount: number
challengeInfo: unknown;
maxRogueChallengeScore: number
}
interface SubAffix {
affixId: number
cnt: number
step?: number
}
interface FlatProp {
type: string
value: number
}
interface PrivacySettingInfo {
displayCollection: boolean
displayRecord: boolean
displayRecordTeam: boolean
displayOnlineStatus: boolean
displayDiary: boolean
}
interface RecordInfo {
achievementCount: number
bookCount: number
avatarCount: number
equipmentCount: number
musicCount: number
relicCount: number
challengeInfo: unknown;
maxRogueChallengeScore: number
}
interface SubAffix {
affixId: number
cnt: number
step?: number
}
interface FlatProp {
type: string
value: number
}
interface RelicFlat {
props: FlatProp[]
setName: string | number
setID: number
}
interface Relic {
mainAffixId: number
subAffixList: SubAffix[]
tid: number
type: number
level: number
_flat: RelicFlat
}
interface SkillTree {
pointId: number
level: number
}
interface Relic {
mainAffixId: number
subAffixList: SubAffix[]
tid: number
type: number
level: number
_flat: RelicFlat
}
interface SkillTree {
pointId: number
level: number
}
interface EquipmentFlat {
props: FlatProp[]
name: string | number
}
interface Equipment {
rank: number
tid: number
promotion: number
level: number
_flat: EquipmentFlat
}
export interface AvatarEnkaDetail {
relicList: Relic[]
level: number
promotion: number
rank?: number
skillTreeList: SkillTree[]
equipment: Equipment
avatarId: number
_assist?: boolean
}
interface DetailInfo {
worldLevel: number
privacySettingInfo: PrivacySettingInfo
headIcon: number
signature: string
avatarDetailList: AvatarEnkaDetail[]
platform: string
recordInfo: RecordInfo
uid: number
level: number
nickname: string
isDisplayAvatar: boolean
friendCount: number
personalCardId: number
}
export interface EnkaResponse {
detailInfo: DetailInfo
ttl: number
uid: string
}
interface Equipment {
rank: number
tid: number
promotion: number
level: number
_flat: EquipmentFlat
}
export interface AvatarEnkaDetail {
relicList: Relic[]
level: number
promotion: number
rank?: number
skillTreeList: SkillTree[]
equipment: Equipment
avatarId: number
_assist?: boolean
}
interface DetailInfo {
worldLevel: number
privacySettingInfo: PrivacySettingInfo
headIcon: number
signature: string
avatarDetailList: AvatarEnkaDetail[]
platform: string
recordInfo: RecordInfo
uid: number
level: number
nickname: string
isDisplayAvatar: boolean
friendCount: number
personalCardId: number
}
export interface EnkaResponse {
detailInfo: DetailInfo
ttl: number
uid: string
}
+27 -27
View File
@@ -1,27 +1,27 @@
export interface ExtraData {
theory_craft?: {
hp: Record<string, number[]>
cycle_count: number
mode: boolean
stage_id: number
custom_lineup?: string[]
}
setting?: {
censorship: boolean
cm: boolean
first_person: boolean
hide_ui: boolean
}
challenge?: {
skip_node: number
challenge_peak_group_id: number
challenge_peak_group_id_list: number[]
}
multi_path?: {
main: number
march_7: number
multi_path_main: number[]
multi_path_march_7: number[]
},
lua?: string | null
}
export interface ExtraData {
theory_craft?: {
hp: Record<string, number[]>
cycle_count: number
mode: boolean
stage_id: number
custom_lineup?: string[]
}
setting?: {
censorship: boolean
cm: boolean
first_person: boolean
hide_ui: boolean
}
challenge?: {
skip_node: number
challenge_peak_group_id: number
challenge_peak_group_id_list: number[]
}
multi_path?: {
main: number
march_7: number
multi_path_main: number[]
multi_path_march_7: number[]
},
lua?: string | null
}
+4
View File
@@ -0,0 +1,4 @@
declare module '*.css';
declare module '*.scss';
declare module '*.module.css';
declare module '*.module.scss';
+20 -20
View File
@@ -1,21 +1,21 @@
export * from "./avatarDetail"
export * from "./srtools"
export * from "./mics"
export * from "./lightconeDetail"
export * from "./relicDetail"
export * from "./enka"
export * from "./card"
export * from "./pfDetail"
export * from "./asDetail"
export * from "./mocDetail"
export * from "./peakDetail"
export * from "./extraData"
export * from "./showcase"
export * from "./srtools"
export * from "./changelog"
export * from "./modelConfig"
export * from "./metaData"
export * from "./monsterDetail"
export * from "./filter"
export * from "./metaData"
export * from "./avatarDetail"
export * from "./srtools"
export * from "./mics"
export * from "./lightconeDetail"
export * from "./relicDetail"
export * from "./enka"
export * from "./card"
export * from "./pfDetail"
export * from "./asDetail"
export * from "./mocDetail"
export * from "./peakDetail"
export * from "./extraData"
export * from "./showcase"
export * from "./srtools"
export * from "./changelog"
export * from "./modelConfig"
export * from "./metaData"
export * from "./monsterDetail"
export * from "./filter"
export * from "./metaData"
export * from "./psConnect"
+51 -51
View File
@@ -1,52 +1,52 @@
export interface LightConeDetail {
ID: number;
Release: boolean;
Name: Record<string, string>;
Desc: Record<string, string>;
Rarity: string;
BaseType: string;
MaxPromotionLevel: number;
MaxRank: number;
Skills: LightconeSkill;
Image: LightconeImage;
Stats: Record<string, LightconeStatLevel>;
}
export interface LightconeSkill {
ID: number;
Name: Record<string, string>;
Desc: Record<string, string>;
AbilityName: string;
Level: Record<string, LightconeSkillLevel>;
}
export interface LightconeSkillLevel {
Level: number;
Param: number[];
Bonus: LightconeSkillBonus[];
}
export interface LightconeSkillBonus {
PropertyType: string;
Value: number;
}
export interface LightconeImage {
ThumbnailPath: string;
ImagePath: string;
IconPath: string;
FigureIconPath: string;
}
export interface LightconeStatLevel {
Promotion: number;
PlayerLevelRequire: number;
WorldLevelRequire: number;
MaxLevel: number;
BaseHP: number;
BaseHPAdd: number;
BaseAttack: number;
BaseAttackAdd: number;
BaseDefence: number;
BaseDefenceAdd: number;
export interface LightConeDetail {
ID: number;
Release: boolean;
Name: Record<string, string>;
Desc: Record<string, string>;
Rarity: string;
BaseType: string;
MaxPromotionLevel: number;
MaxRank: number;
Skills: LightconeSkill;
Image: LightconeImage;
Stats: Record<string, LightconeStatLevel>;
}
export interface LightconeSkill {
ID: number;
Name: Record<string, string>;
Desc: Record<string, string>;
AbilityName: string;
Level: Record<string, LightconeSkillLevel>;
}
export interface LightconeSkillLevel {
Level: number;
Param: number[];
Bonus: LightconeSkillBonus[];
}
export interface LightconeSkillBonus {
PropertyType: string;
Value: number;
}
export interface LightconeImage {
ThumbnailPath: string;
ImagePath: string;
IconPath: string;
FigureIconPath: string;
}
export interface LightconeStatLevel {
Promotion: number;
PlayerLevelRequire: number;
WorldLevelRequire: number;
MaxLevel: number;
BaseHP: number;
BaseHPAdd: number;
BaseAttack: number;
BaseAttackAdd: number;
BaseDefence: number;
BaseDefenceAdd: number;
}
+59 -59
View File
@@ -1,59 +1,59 @@
export interface Metadata {
BaseType: Record<string, BaseTypeData>;
DamageType: Record<string, DamageTypeData>;
MainAffix: Record<string, Record<string, MainAffixData>>;
SubAffix: Record<string, Record<string, SubAffixData>>;
SkillConfig: Record<string, SkillMaxLevelData>;
Stage: Record<string, StageData>;
HardLevelConfig: Record<string, Record<string, HardLevelData>>;
EliteConfig: Record<string, EliteData>;
}
export interface HardLevelData {
AttackRatio: number
DefenceRatio: number
HPRatio: number
SpeedRatio: number
StanceRatio: number
}
export interface EliteData {
AttackRatio: number
DefenceRatio: number
HPRatio: number
SpeedRatio: number
StanceRatio: number
}
export interface StageData {
ID: number;
Name: Record<string, string>;
}
export interface BaseTypeData {
Text: Record<string, string>;
Icon: string;
}
export interface DamageTypeData {
Name: Record<string, string>;
Icon: string;
}
export interface MainAffixData {
Property: string;
BaseValue: number;
LevelAdd: number;
}
export interface SubAffixData {
Property: string;
BaseValue: number;
StepValue: number;
StepNum: number;
}
export interface SkillMaxLevelData {
MaxLevel: number;
IndexSlot: number;
}
export interface Metadata {
BaseType: Record<string, BaseTypeData>;
DamageType: Record<string, DamageTypeData>;
MainAffix: Record<string, Record<string, MainAffixData>>;
SubAffix: Record<string, Record<string, SubAffixData>>;
SkillConfig: Record<string, SkillMaxLevelData>;
Stage: Record<string, StageData>;
HardLevelConfig: Record<string, Record<string, HardLevelData>>;
EliteConfig: Record<string, EliteData>;
}
export interface HardLevelData {
AttackRatio: number
DefenceRatio: number
HPRatio: number
SpeedRatio: number
StanceRatio: number
}
export interface EliteData {
AttackRatio: number
DefenceRatio: number
HPRatio: number
SpeedRatio: number
StanceRatio: number
}
export interface StageData {
ID: number;
Name: Record<string, string>;
}
export interface BaseTypeData {
Text: Record<string, string>;
Icon: string;
}
export interface DamageTypeData {
Name: Record<string, string>;
Icon: string;
}
export interface MainAffixData {
Property: string;
BaseValue: number;
LevelAdd: number;
}
export interface SubAffixData {
Property: string;
BaseValue: number;
StepValue: number;
StepNum: number;
}
export interface SkillMaxLevelData {
MaxLevel: number;
IndexSlot: number;
}
+121 -121
View File
@@ -1,122 +1,122 @@
export type AvatarDataStore = {
rank: number,
skills: Record<string, number>
}
export type LightconeStore = {
level: number;
item_id: number;
rank: number;
promotion: number;
}
export type SubAffixStore = {
sub_affix_id: number;
count: number;
step: number;
}
export type RelicStore = {
level: number;
relic_id: number;
relic_set_id: number;
main_affix_id: number;
sub_affixes: SubAffixStore[];
}
export type AvatarProfileStore = {
profile_name: string,
lightcone: LightconeStore | null,
relics: Record<string, RelicStore>
}
export type AvatarStore = {
owner_uid?: number;
avatar_id: number;
data: AvatarDataStore;
level: number;
promotion: number;
techniques: number[];
sp_value: number;
sp_max: number;
can_change_sp: boolean;
enhanced: string;
profileSelect: number;
profileList: AvatarProfileStore[]
}
export type MonsterStore = {
monster_id: number;
level: number;
amount: number;
}
export type DynamicKeyStore = {
key: string;
value: number;
}
export type BattleBuffStore = {
level: number;
id: number;
dynamic_key?: DynamicKeyStore;
}
export type MOCConfigStore = {
event_id: number;
challenge_id: number;
floor_side: string;
use_turbulence_buff: boolean;
use_cycle_count: boolean;
blessings: BattleBuffStore[]
cycle_count: number;
stage_id: number;
monsters: MonsterStore[][];
}
export type PFConfigStore = {
event_id: number;
challenge_id: number;
buff_id: number;
floor_side: string;
blessings: BattleBuffStore[]
cycle_count: number;
stage_id: number;
monsters: MonsterStore[][];
}
export type ASConfigStore = {
event_id: number;
challenge_id: number;
buff_id: number;
floor_side: string;
blessings: BattleBuffStore[]
cycle_count: number;
stage_id: number;
monsters: MonsterStore[][];
}
export type PEAKConfigStore = {
event_id: number;
challenge_id: number;
buff_id: number;
boss_mode: string;
blessings: BattleBuffStore[]
cycle_count: number;
stage_id: number;
monsters: MonsterStore[][];
}
export type CEConfigStore = {
blessings: BattleBuffStore[]
cycle_count: number;
stage_id: number;
monsters: MonsterStore[][];
}
export interface Mics {
avatars: Record<string, AvatarStore>,
battle_type: string;
moc_config: MOCConfigStore;
pf_config: PFConfigStore;
as_config: ASConfigStore;
ce_config: CEConfigStore;
peak_config: PEAKConfigStore
export type AvatarDataStore = {
rank: number,
skills: Record<string, number>
}
export type LightconeStore = {
level: number;
item_id: number;
rank: number;
promotion: number;
}
export type SubAffixStore = {
sub_affix_id: number;
count: number;
step: number;
}
export type RelicStore = {
level: number;
relic_id: number;
relic_set_id: number;
main_affix_id: number;
sub_affixes: SubAffixStore[];
}
export type AvatarProfileStore = {
profile_name: string,
lightcone: LightconeStore | null,
relics: Record<string, RelicStore>
}
export type AvatarStore = {
owner_uid?: number;
avatar_id: number;
data: AvatarDataStore;
level: number;
promotion: number;
techniques: number[];
sp_value: number;
sp_max: number;
can_change_sp: boolean;
enhanced: string;
profileSelect: number;
profileList: AvatarProfileStore[]
}
export type MonsterStore = {
monster_id: number;
level: number;
amount: number;
}
export type DynamicKeyStore = {
key: string;
value: number;
}
export type BattleBuffStore = {
level: number;
id: number;
dynamic_key?: DynamicKeyStore;
}
export type MOCConfigStore = {
event_id: number;
challenge_id: number;
floor_side: string;
use_turbulence_buff: boolean;
use_cycle_count: boolean;
blessings: BattleBuffStore[]
cycle_count: number;
stage_id: number;
monsters: MonsterStore[][];
}
export type PFConfigStore = {
event_id: number;
challenge_id: number;
buff_id: number;
floor_side: string;
blessings: BattleBuffStore[]
cycle_count: number;
stage_id: number;
monsters: MonsterStore[][];
}
export type ASConfigStore = {
event_id: number;
challenge_id: number;
buff_id: number;
floor_side: string;
blessings: BattleBuffStore[]
cycle_count: number;
stage_id: number;
monsters: MonsterStore[][];
}
export type PEAKConfigStore = {
event_id: number;
challenge_id: number;
buff_id: number;
boss_mode: string;
blessings: BattleBuffStore[]
cycle_count: number;
stage_id: number;
monsters: MonsterStore[][];
}
export type CEConfigStore = {
blessings: BattleBuffStore[]
cycle_count: number;
stage_id: number;
monsters: MonsterStore[][];
}
export interface Mics {
avatars: Record<string, AvatarStore>,
battle_type: string;
moc_config: MOCConfigStore;
pf_config: PFConfigStore;
as_config: ASConfigStore;
ce_config: CEConfigStore;
peak_config: PEAKConfigStore
}
+3 -1
View File
@@ -1,4 +1,4 @@
import { InfiniteWave, MazeBuff } from "./pfDetail";
import { InfiniteWave, Invasion, MazeBuff } from "./pfDetail";
export interface MOCGroupDetail {
ID: number;
@@ -40,6 +40,7 @@ export interface MoCTarget {
Param: number[];
}
export interface MoCEvent {
ID: number;
Name: Record<string, string>;
@@ -48,5 +49,6 @@ export interface MoCEvent {
Level: number;
Release: boolean;
MonsterList: number[][];
Invasion: Invasion | null;
Infinite: InfiniteWave[] | null;
}
+6 -6
View File
@@ -1,7 +1,7 @@
export type ModalConfig = {
id: string
title: string
isOpen: boolean
onClose: () => void
content: React.ReactNode
export type ModalConfig = {
id: string
title: string
isOpen: boolean
onClose: () => void
content: React.ReactNode
}
+44 -44
View File
@@ -1,45 +1,45 @@
export interface MonsterDetail {
ID: number;
Name: Record<string, string>;
EliteGroup: number;
HardLevelGroup: number;
StanceWeakList: string[];
Modify: MonsterModify;
Rank: string;
StanceCount: number;
StanceType: string;
Image: MonsterImage;
Base: MonsterBase;
}
export interface MonsterModify {
AttackModifyRatio: number;
DefenceModifyRatio: number;
HPModifyRatio: number;
SpeedModifyRatio: number;
StanceModifyRatio: number;
SpeedModifyValue: number;
StanceModifyValue: number;
}
export interface MonsterImage {
IconPath: string;
RoundIconPath: string;
ImagePath: string;
ManikinImagePath: string;
}
export interface MonsterBase {
AttackBase: number;
DefenceBase: number;
HPBase: number;
SpeedBase: number;
StanceBase: number;
CriticalDamageBase: number;
StatusResistanceBase: number;
SpeedModifyValue: number;
StanceModifyValue: number;
InitialDelayRatio: number;
export interface MonsterDetail {
ID: number;
Name: Record<string, string>;
EliteGroup: number;
HardLevelGroup: number;
StanceWeakList: string[];
Modify: MonsterModify;
Rank: string;
StanceCount: number;
StanceType: string;
Image: MonsterImage;
Base: MonsterBase;
}
export interface MonsterModify {
AttackModifyRatio: number;
DefenceModifyRatio: number;
HPModifyRatio: number;
SpeedModifyRatio: number;
StanceModifyRatio: number;
SpeedModifyValue: number;
StanceModifyValue: number;
}
export interface MonsterImage {
IconPath: string;
RoundIconPath: string;
ImagePath: string;
ManikinImagePath: string;
}
export interface MonsterBase {
AttackBase: number;
DefenceBase: number;
HPBase: number;
SpeedBase: number;
StanceBase: number;
CriticalDamageBase: number;
StatusResistanceBase: number;
SpeedModifyValue: number;
StanceModifyValue: number;
InitialDelayRatio: number;
}
+41 -40
View File
@@ -1,41 +1,42 @@
import { BattleTarget, InfiniteWave, MazeBuff } from "./pfDetail";
export interface PeakGroupDetail {
ID: number;
ChallengeGroupType: string;
Name: Record<string, string>;
PreLevel: PeakMazeConfig[];
BossLevel: PeakMazeConfig | null;
BossConfig: PeakBossConfig | null;
}
export interface PeakMazeConfig {
ID: number;
Name: Record<string, string>;
BattleTarget: BattleTarget[];
DamageType: string[];
MazeBuff: MazeBuff[];
TurnLimit: number;
EventList: PeakEvent[];
}
export interface PeakBossConfig {
ID: number;
Name: Record<string, string>;
BuffList: MazeBuff[];
BattleTarget: BattleTarget[];
MazeBuff: MazeBuff[];
TurnLimit: number;
EventList: PeakEvent[];
}
export interface PeakEvent {
ID: number;
Name: Record<string, string>;
HardLevelGroup: number;
EliteGroup: number;
Level: number;
Release: boolean;
MonsterList: number[][];
Infinite: InfiniteWave[] | null;
import { BattleTarget, InfiniteWave, Invasion, MazeBuff } from "./pfDetail";
export interface PeakGroupDetail {
ID: number;
ChallengeGroupType: string;
Name: Record<string, string>;
PreLevel: PeakMazeConfig[];
BossLevel: PeakMazeConfig | null;
BossConfig: PeakBossConfig | null;
}
export interface PeakMazeConfig {
ID: number;
Name: Record<string, string>;
BattleTarget: BattleTarget[];
DamageType: string[];
MazeBuff: MazeBuff[];
TurnLimit: number;
EventList: PeakEvent[];
}
export interface PeakBossConfig {
ID: number;
Name: Record<string, string>;
BuffList: MazeBuff[];
BattleTarget: BattleTarget[];
MazeBuff: MazeBuff[];
TurnLimit: number;
EventList: PeakEvent[];
}
export interface PeakEvent {
ID: number;
Name: Record<string, string>;
HardLevelGroup: number;
EliteGroup: number;
Level: number;
Release: boolean;
MonsterList: number[][];
Invasion: Invasion | null;
Infinite: InfiniteWave[] | null;
}
+81 -75
View File
@@ -1,75 +1,81 @@
export interface PFGroupDetail {
ID: number;
ChallengeGroupType: string;
Name: Record<string, string>;
BeginTime: string;
EndTime: string;
SubOption: MazeBuff[];
Option: MazeBuff[];
Level: PFLevel[];
Tierce: PFTierceLevel | null;
}
export interface MazeBuff {
ID: number;
Param: number[];
Icon: string;
Name: Record<string, string>;
Desc: Record<string, string>;
}
export interface PFTierceLevel {
ID: number;
PreChallenge: number;
Name: Record<string, string>;
Target: StoryTarget[];
DamageType: string[];
TurnLimit: number;
EventList: PFEvent[];
}
export interface PFLevel {
Floor: number;
ID: number;
StageNum: number;
Name: Record<string, string>;
Target: StoryTarget[];
DamageType1: string[];
DamageType2: string[];
MazeBuff: MazeBuff[];
EventList1: PFEvent[];
EventList2: PFEvent[];
TurnLimit: number;
BattleTarget: BattleTarget[];
ClearScore: number;
}
export interface StoryTarget {
ID: number;
Name: Record<string, string>;
Param: number[];
}
export interface BattleTarget {
ID: number;
Param: number[];
Name: Record<string, string>;
}
export interface PFEvent {
ID: number;
Name: Record<string, string>;
HardLevelGroup: number;
EliteGroup: number;
Level: number;
Release: boolean;
MonsterList: number[][];
Infinite?: InfiniteWave[] | null;
}
export interface InfiniteWave {
ID: number;
MaxMonsterCount: number;
MonsterList: number[];
EliteGroup: number;
}
export interface PFGroupDetail {
ID: number;
ChallengeGroupType: string;
Name: Record<string, string>;
BeginTime: string;
EndTime: string;
SubOption: MazeBuff[];
Option: MazeBuff[];
Level: PFLevel[];
Tierce: PFTierceLevel | null;
}
export interface MazeBuff {
ID: number;
Param: number[];
Icon: string;
Name: Record<string, string>;
Desc: Record<string, string>;
}
export interface Invasion {
ID: number;
MazeBuff: MazeBuff;
Desc: Record<string, string>;
}
export interface PFTierceLevel {
ID: number;
PreChallenge: number;
Name: Record<string, string>;
Target: StoryTarget[];
DamageType: string[];
TurnLimit: number;
EventList: PFEvent[];
}
export interface PFLevel {
Floor: number;
ID: number;
StageNum: number;
Name: Record<string, string>;
Target: StoryTarget[];
DamageType1: string[];
DamageType2: string[];
MazeBuff: MazeBuff[];
EventList1: PFEvent[];
EventList2: PFEvent[];
TurnLimit: number;
BattleTarget: BattleTarget[];
ClearScore: number;
}
export interface StoryTarget {
ID: number;
Name: Record<string, string>;
Param: number[];
}
export interface BattleTarget {
ID: number;
Param: number[];
Name: Record<string, string>;
}
export interface InfiniteWave {
ID: number;
MaxMonsterCount: number;
MonsterList: number[];
EliteGroup: number;
}
export interface PFEvent {
ID: number;
Name: Record<string, string>;
HardLevelGroup: number;
EliteGroup: number;
Level: number;
Release: boolean;
MonsterList: number[][];
Invasion: Invasion | null;
Infinite?: InfiniteWave[] | null;
}

Some files were not shown because too many files have changed in this diff Show More