This commit is contained in:
18
src/app/api/[locale]/characters/[id]/route.ts
Normal file
18
src/app/api/[locale]/characters/[id]/route.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { loadCharacters } from '@/lib/characterLoader'
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string, locale: string }> }
|
||||
) {
|
||||
|
||||
const { id, locale } = await params
|
||||
const characters = await loadCharacters([id], locale)
|
||||
const char = characters[id]
|
||||
|
||||
if (!char) {
|
||||
return NextResponse.json({ error: 'Character not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json(char)
|
||||
}
|
||||
20
src/app/api/[locale]/characters/route.ts
Normal file
20
src/app/api/[locale]/characters/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { loadCharacters } from "@/lib/characterLoader";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ locale: string }> }) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const charIds = body.charIds as string[];
|
||||
const { locale } = await params;
|
||||
|
||||
if (!Array.isArray(charIds) || charIds.some(id => typeof id !== 'string')) {
|
||||
return NextResponse.json({ error: 'Invalid charIds' }, { status: 400 });
|
||||
}
|
||||
|
||||
const characters = await loadCharacters(charIds, locale);
|
||||
|
||||
return NextResponse.json(characters);
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Failed to load characters' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
18
src/app/api/[locale]/lightcones/[id]/route.ts
Normal file
18
src/app/api/[locale]/lightcones/[id]/route.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { loadLightcones } from '@/lib/lighconeLoader'
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string, locale: string }> }
|
||||
) {
|
||||
|
||||
const { id, locale } = await params
|
||||
const lightcones = await loadLightcones([id], locale)
|
||||
const lightcone = lightcones[id]
|
||||
|
||||
if (!lightcone) {
|
||||
return NextResponse.json({ error: 'Lightcone not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json(lightcone)
|
||||
}
|
||||
20
src/app/api/[locale]/lightcones/route.ts
Normal file
20
src/app/api/[locale]/lightcones/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { loadLightcones } from "@/lib/lighconeLoader";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ locale: string }> }) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const lightconeIds = body.lightconeIds as string[];
|
||||
const { locale } = await params;
|
||||
|
||||
if (!Array.isArray(lightconeIds) || lightconeIds.some(id => typeof id !== 'string')) {
|
||||
return NextResponse.json({ error: 'Invalid lightconeIds' }, { status: 400 });
|
||||
}
|
||||
|
||||
const lightcones = await loadLightcones(lightconeIds, locale);
|
||||
|
||||
return NextResponse.json(lightcones);
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Failed to load lightcones' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
18
src/app/api/[locale]/relics/[id]/route.ts
Normal file
18
src/app/api/[locale]/relics/[id]/route.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { loadRelics } from '@/lib/relicLoader'
|
||||
|
||||
export async function GET(
|
||||
req: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string, locale: string }> }
|
||||
) {
|
||||
|
||||
const { id, locale } = await params
|
||||
const relics = await loadRelics([id], locale)
|
||||
const relic = relics[id]
|
||||
|
||||
if (!relic) {
|
||||
return NextResponse.json({ error: 'Relic not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
return NextResponse.json(relic)
|
||||
}
|
||||
20
src/app/api/[locale]/relics/route.ts
Normal file
20
src/app/api/[locale]/relics/route.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { loadRelics } from "@/lib/relicLoader";
|
||||
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ locale: string }> }) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const relicIds = body.relicIds as string[];
|
||||
const { locale } = await params;
|
||||
|
||||
if (!Array.isArray(relicIds) || relicIds.some(id => typeof id !== 'string')) {
|
||||
return NextResponse.json({ error: 'Invalid relicIds' }, { status: 400 });
|
||||
}
|
||||
|
||||
const relics = await loadRelics(relicIds, locale);
|
||||
|
||||
return NextResponse.json(relics);
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Failed to load relics' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
11
src/app/api/config-maze/route.ts
Normal file
11
src/app/api/config-maze/route.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { loadConfigMaze } from "@/lib/configMazeLoader";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const configMaze = await loadConfigMaze();
|
||||
return NextResponse.json(configMaze);
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Failed to load config maze' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
11
src/app/api/main-affixes/route.ts
Normal file
11
src/app/api/main-affixes/route.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { loadMainAffix } from "@/lib/affixLoader";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const mainAffix = await loadMainAffix();
|
||||
return NextResponse.json(mainAffix);
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Failed to load main affix' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
73
src/app/api/proxy/route.ts
Normal file
73
src/app/api/proxy/route.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
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 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 }
|
||||
)
|
||||
}
|
||||
|
||||
let response
|
||||
|
||||
switch (method.toUpperCase()) {
|
||||
case 'GET':
|
||||
const queryString = new URLSearchParams(payload as any).toString()
|
||||
const fullUrl = queryString ? `${url}?${queryString}` : url
|
||||
response = await axios.get(fullUrl)
|
||||
break
|
||||
case 'POST':
|
||||
response = await axios.post(url, payload)
|
||||
break
|
||||
case 'PUT':
|
||||
response = await axios.put(url, payload)
|
||||
break
|
||||
case 'DELETE':
|
||||
response = await axios.delete(url, { data: payload })
|
||||
break
|
||||
default:
|
||||
return NextResponse.json({ error: `Unsupported method: ${method}` }, { status: 405 })
|
||||
}
|
||||
|
||||
return NextResponse.json(response.data)
|
||||
} catch (err: any) {
|
||||
return NextResponse.json({ error: err.message || 'Proxy failed' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
11
src/app/api/sub-affixes/route.ts
Normal file
11
src/app/api/sub-affixes/route.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { loadSubAffix } from "@/lib/affixLoader";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const subAffix = await loadSubAffix();
|
||||
return NextResponse.json(subAffix);
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Failed to load sub affix' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
12
src/app/eidolons-info/page.tsx
Normal file
12
src/app/eidolons-info/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client"
|
||||
import Image from "next/image";
|
||||
import EidolonsInfo from "@/components/eidolonsInfo";
|
||||
import { useRouter } from 'next/navigation'
|
||||
export default function EidolonsInfoPage() {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<div className="w-full">
|
||||
<EidolonsInfo/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 195 KiB |
@@ -1,26 +1,63 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #ffffff;
|
||||
--foreground: #171717;
|
||||
@plugin "daisyui" {
|
||||
themes: winter --default, night --prefersdark, cupcake, coffee;
|
||||
}
|
||||
@plugin 'tailwind-scrollbar' {
|
||||
nocompatible: true;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-geist-sans);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #9ca3af;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@tailwind utilities;
|
||||
|
||||
@layer components {
|
||||
/* .my-react-select-container {
|
||||
} */
|
||||
.my-react-select-container .my-react-select__control {
|
||||
@apply bg-white dark:bg-neutral-700 border-2 border-neutral-300 dark:border-neutral-700 hover:border-neutral-400 dark:hover:border-neutral-500;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
.my-react-select-container .my-react-select__control--is-focused {
|
||||
@apply border-neutral-500 hover:border-neutral-500 dark:border-neutral-400 dark:hover:border-neutral-400 shadow-none;
|
||||
}
|
||||
|
||||
.my-react-select-container .my-react-select__menu {
|
||||
@apply bg-neutral-100 dark:bg-neutral-700 border-2 border-neutral-300 dark:border-neutral-600;
|
||||
}
|
||||
|
||||
.my-react-select-container .my-react-select__option {
|
||||
@apply text-neutral-600 dark:text-neutral-200 bg-neutral-100 hover:bg-neutral-200 dark:bg-neutral-700 dark:hover:bg-neutral-800;
|
||||
}
|
||||
/* .my-react-select-container .my-react-select__option--is-focused {
|
||||
@apply bg-neutral-200 dark:bg-neutral-800;
|
||||
} */
|
||||
|
||||
.my-react-select-container .my-react-select__indicator-separator {
|
||||
@apply bg-neutral-400;
|
||||
}
|
||||
.my-react-select-container .my-react-select__menu {
|
||||
z-index: 999;
|
||||
}
|
||||
.my-react-select-container .my-react-select__input-container,
|
||||
.my-react-select-container .my-react-select__placeholder,
|
||||
.my-react-select-container .my-react-select__single-value {
|
||||
@apply text-neutral-600 dark:text-neutral-200;
|
||||
}
|
||||
}
|
||||
BIN
src/app/icon.png
Normal file
BIN
src/app/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 390 KiB |
@@ -1,6 +1,14 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import Header from "../components/header";
|
||||
import { ClientThemeWrapper, ThemeProvider } from "@/components/themeController";
|
||||
import Footer from "@/components/footer";
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getLocale, getMessages } from "next-intl/server";
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import AvatarBar from "@/components/avatarBar";
|
||||
import ActionBar from "@/components/actionBar";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -13,21 +21,45 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Create Next App",
|
||||
description: "Generated by create next app",
|
||||
title: "Firefly SrTools",
|
||||
description: "SrTools by Kain",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const messages = await getMessages();
|
||||
const locale = await getLocale()
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{children}
|
||||
<NextIntlClientProvider messages={messages}>
|
||||
<ThemeProvider>
|
||||
<ClientThemeWrapper>
|
||||
<div className="min-h-screen w-full">
|
||||
<Header />
|
||||
<div className="grid grid-cols-12 w-full">
|
||||
<div className="col-span-3 sticky top-0 self-start h-fit">
|
||||
<AvatarBar />
|
||||
</div>
|
||||
<div className="col-span-9">
|
||||
<ActionBar />
|
||||
{children}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
</ClientThemeWrapper>
|
||||
</ThemeProvider>
|
||||
</NextIntlClientProvider>
|
||||
<ToastContainer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
110
src/app/page.tsx
110
src/app/page.tsx
@@ -1,103 +1,13 @@
|
||||
import Image from "next/image";
|
||||
"use client"
|
||||
import AvatarInfo from "@/components/avatarInfo";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
|
||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={180}
|
||||
height={38}
|
||||
priority
|
||||
/>
|
||||
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
|
||||
<li className="mb-2 tracking-[-.01em]">
|
||||
Get started by editing{" "}
|
||||
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
|
||||
src/app/page.tsx
|
||||
</code>
|
||||
.
|
||||
</li>
|
||||
<li className="tracking-[-.01em]">
|
||||
Save and see your changes instantly.
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
||||
<a
|
||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
Deploy now
|
||||
</a>
|
||||
<a
|
||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Read our docs
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/file.svg"
|
||||
alt="File icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Learn
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/window.svg"
|
||||
alt="Window icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Examples
|
||||
</a>
|
||||
<a
|
||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
aria-hidden
|
||||
src="/globe.svg"
|
||||
alt="Globe icon"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Go to nextjs.org →
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
const router = useRouter()
|
||||
return (
|
||||
<div className="w-full">
|
||||
<AvatarInfo></AvatarInfo>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
13
src/app/relics-info/page.tsx
Normal file
13
src/app/relics-info/page.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
"use client"
|
||||
import Image from "next/image";
|
||||
|
||||
import RelicsInfo from "@/components/relicsInfo";
|
||||
import { useRouter } from 'next/navigation'
|
||||
export default function RelicsInfoPage() {
|
||||
const router = useRouter()
|
||||
return (
|
||||
<div className="w-full">
|
||||
<RelicsInfo></RelicsInfo>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
372
src/components/actionBar/index.tsx
Normal file
372
src/components/actionBar/index.tsx
Normal file
@@ -0,0 +1,372 @@
|
||||
"use client";
|
||||
|
||||
import useListAvatarStore from "@/stores/avatarStore";
|
||||
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 { 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";
|
||||
|
||||
|
||||
export default function ActionBar() {
|
||||
const router = useRouter()
|
||||
const { avatarSelected, listRawAvatar } = useListAvatarStore()
|
||||
const { setListCopyAvatar } = useCopyProfileStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { locale, setLocale } = useLocaleStore()
|
||||
const { isOpenCreateProfile, setIsOpenCreateProfile, isOpenCopy, setIsOpenCopy } = useModelStore()
|
||||
const { avatars, setAvatar } = useUserDataStore()
|
||||
const [profileName, setProfileName] = useState("");
|
||||
const [formState, setFormState] = useState("EDIT");
|
||||
const [profileEdit, setProfileEdit] = useState(-1);
|
||||
const { isConnectPS, setIsConnectPS } = 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();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle ESC key to close modal
|
||||
useEffect(() => {
|
||||
if (!isOpenCreateProfile) {
|
||||
handleCloseModal("update_profile_modal");
|
||||
return;
|
||||
}
|
||||
if (!isOpenCopy) {
|
||||
handleCloseModal("copy_profile_modal");
|
||||
return;
|
||||
}
|
||||
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isOpenCreateProfile) {
|
||||
handleCloseModal("update_profile_modal");
|
||||
}
|
||||
if (event.key === 'Escape' && isOpenCopy) {
|
||||
handleCloseModal("copy_profile_modal");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleEscKey);
|
||||
|
||||
return () => window.removeEventListener('keydown', handleEscKey);
|
||||
}, [isOpenCreateProfile]);
|
||||
|
||||
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}`)
|
||||
setIsConnectPS(false)
|
||||
}
|
||||
} else {
|
||||
const res = await connectToPS()
|
||||
if (res.success) {
|
||||
toast.success(transI18n("connectedSuccess"))
|
||||
setIsConnectPS(true)
|
||||
} else {
|
||||
toast.error(`${transI18n("connectedFailed")}: ${res.message}`)
|
||||
setIsConnectPS(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full mb-2">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 items-center justify-items-center">
|
||||
<div className="grid grid-rows-2 lg:grid-rows-1 items-center w-full">
|
||||
<div className="flex justify-between gap-10 w-full">
|
||||
<div className="flex items-center p-1 h-full lg:p-2 opacity-80 lg:hover:opacity-100 cursor-pointer text-base md:text-lg lg:text-xl">
|
||||
{avatarSelected && (
|
||||
<>
|
||||
<Image
|
||||
src={"https://api.hakush.in/hsr/UI/element/" + avatarSelected.damageType.toLowerCase() + ".webp"}
|
||||
alt={'fire'}
|
||||
className="h-[40px] w-[40px] object-contain"
|
||||
width={100}
|
||||
height={100}
|
||||
/>
|
||||
<div className="flex items-center justify-center h-full w-full">
|
||||
<p className="text-center font-bold text-xl">
|
||||
{transI18n(avatarSelected.baseType.toLowerCase())}
|
||||
</p>
|
||||
<div className="text-center font-bold text-xl">{" / "}</div>
|
||||
<ParseText
|
||||
locale={locale}
|
||||
text={getNameChar(locale, avatarSelected).toWellFormed()}
|
||||
className={"font-bold text-xl"}
|
||||
/>
|
||||
{avatarSelected?.id && (
|
||||
<div className="text-center italic text-sm ml-2"> {`(${avatarSelected.id})`}</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 ml-4 w-full">
|
||||
<span className="text-base opacity-70 font-bold w-16">{transI18n("profile")}:</span>
|
||||
<div className="dropdown dropdown-end w-full">
|
||||
<div
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
className="btn btn-warning border-info btn-soft gap-1 min-w-0"
|
||||
>
|
||||
<span className="truncate max-w-24 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 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={(e) => 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)
|
||||
setListCopyAvatar(listRawAvatar)
|
||||
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>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
{/* Action Buttons */}
|
||||
<div className="grid grid-cols-2 xl: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 disabled className="btn btn-success btn-sm cursor-not-allowed italic">{transI18n("skills")} ({transI18n("comingSoon")})</button>
|
||||
<button disabled className="btn btn-success btn-sm cursor-not-allowed italic">{transI18n("showcaseCard")} {transI18n("comingSoon")}</button>
|
||||
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm"> {isConnectPS ? transI18n("sync") : transI18n("connectPs")}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<dialog id="update_profile_modal" className="modal sm:modal-middle backdrop-blur-sm">
|
||||
<div className="modal-box w-11/12 max-w-7xl 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={() => {
|
||||
setIsOpenCreateProfile(false)
|
||||
handleCloseModal("update_profile_modal")
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</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-gradient-to-r from-pink-400 to-cyan-400">
|
||||
{formState === "CREATE" ? transI18n("createNewProfile") : transI18n("editProfile")}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="copy_profile_modal" className="modal">
|
||||
<div className="modal-box w-11/12 max-w-7xl 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={() => {
|
||||
setIsOpenCopy(false)
|
||||
handleCloseModal("copy_profile_modal")
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</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-gradient-to-r from-pink-400 to-cyan-400">
|
||||
{transI18n("copyProfiles").toUpperCase()}
|
||||
</h3>
|
||||
</div>
|
||||
<CopyImport />
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
164
src/components/avatarBar/index.tsx
Normal file
164
src/components/avatarBar/index.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
"use client"
|
||||
import { getCharacterListApi, fetchCharactersByIdsNative, getConfigMazeApi, getLightconeListApi, fetchLightconesByIdsNative, fetchRelicsByIdsNative, getRelicSetListApi, getMainAffixApi, getSubAffixApi } from "@/lib/api"
|
||||
import Image from "next/image"
|
||||
import { useEffect, useState } from "react"
|
||||
import CharacterCard from "../card/characterCard"
|
||||
import useLocaleStore from "@/stores/localeStore"
|
||||
import { listCurrentLanguageApi } from "@/lib/constant"
|
||||
import useAvatarStore from "@/stores/avatarStore"
|
||||
import useUserDataStore from "@/stores/userDataStore"
|
||||
import { converterToAvatarStore, getAvatarNotExist } from "@/helper"
|
||||
import useLightconeStore from "@/stores/lightconeStore"
|
||||
import useRelicStore from "@/stores/relicStore"
|
||||
import useAffixStore from "@/stores/affixStore"
|
||||
import useMazeStore from "@/stores/mazeStore"
|
||||
import { useTranslations } from "next-intl"
|
||||
|
||||
export default function AvatarBar() {
|
||||
const [listElement, setListElement] = useState<Record<string, boolean>>({ "fire": false, "ice": false, "imaginary": false, "physical": false, "quantum": false, "thunder": false, "wind": false })
|
||||
const [listPath, setListPath] = useState<Record<string, boolean>>({ "knight": false, "mage": false, "priest": false, "rogue": false, "shaman": false, "warlock": false, "warrior": false, "memory": false })
|
||||
const { listAvatar, setListAvatar, setAvatarSelected, setFilter, filter, setAllMapAvatarInfo, avatarSelected } = useAvatarStore()
|
||||
const { setAvatars, avatars } = useUserDataStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { locale } = useLocaleStore()
|
||||
const { setListLightcone, setAllMapLightconeInfo } = useLightconeStore()
|
||||
const { setListRelic, setAllMapRelicInfo } = useRelicStore()
|
||||
const { setMapMainAffix, setMapSubAffix } = useAffixStore()
|
||||
const { setAllData } = useMazeStore()
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
// Maze
|
||||
const maze = await getConfigMazeApi()
|
||||
setAllData(maze)
|
||||
|
||||
// Affix
|
||||
const mapMainAffix = await getMainAffixApi()
|
||||
setMapMainAffix(mapMainAffix)
|
||||
|
||||
const mapSubAffix = await getSubAffixApi()
|
||||
setMapSubAffix(mapSubAffix)
|
||||
}
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
// Avatar
|
||||
const listAvatar = await getCharacterListApi()
|
||||
listAvatar.sort((a, b) => {
|
||||
const aHasRelease = typeof a.release === 'number';
|
||||
const bHasRelease = typeof b.release === 'number';
|
||||
if (!aHasRelease && !bHasRelease) return 0;
|
||||
if (!aHasRelease) return -1;
|
||||
if (!bHasRelease) return 1;
|
||||
return b.release! - a.release!;
|
||||
});
|
||||
|
||||
const mapAvatar = await fetchCharactersByIdsNative(listAvatar.map((item) => item.id), listCurrentLanguageApi[locale.toLowerCase()])
|
||||
setListAvatar(listAvatar)
|
||||
setAllMapAvatarInfo(mapAvatar)
|
||||
const avatarStore = converterToAvatarStore(getAvatarNotExist())
|
||||
if (Object.keys(avatarStore).length > 0) {
|
||||
setAvatars({ ...avatars, ...avatarStore })
|
||||
}
|
||||
if (!avatarSelected) {
|
||||
setAvatarSelected(listAvatar[0])
|
||||
}
|
||||
|
||||
// Lightcone
|
||||
const listLightcone = await getLightconeListApi()
|
||||
listLightcone.sort((a, b) => Number(b.id) - Number(a.id))
|
||||
setListLightcone(listLightcone)
|
||||
const mapLightcone = await fetchLightconesByIdsNative(listLightcone.map((item) => item.id), listCurrentLanguageApi[locale.toLowerCase()])
|
||||
setAllMapLightconeInfo(mapLightcone)
|
||||
|
||||
|
||||
// Relic
|
||||
const listRelic = await getRelicSetListApi()
|
||||
setListRelic(listRelic)
|
||||
const mapRelic = await fetchRelicsByIdsNative(listRelic.map((item) => item.id), listCurrentLanguageApi[locale.toLowerCase()])
|
||||
setAllMapRelicInfo(mapRelic)
|
||||
}
|
||||
fetchData()
|
||||
}, [locale])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setFilter({ ...filter, locale: locale, element: Object.keys(listElement).filter((key) => listElement[key]), path: Object.keys(listPath).filter((key) => listPath[key]) })
|
||||
}, [locale, listElement, listPath])
|
||||
|
||||
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 max-w-xs"
|
||||
value={filter.name}
|
||||
onChange={(e) => setFilter({ ...filter, name: e.target.value, locale: locale })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md: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(listElement).map(([key, value], index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setListElement((prev) => ({ ...prev, [key]: !prev[key] }))
|
||||
}}
|
||||
className="hover:bg-gray-600 grid items-center justify-items-center cursor-pointer rounded-md shadow-lg"
|
||||
style={{
|
||||
backgroundColor: listElement[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<Image src={"https://api.hakush.in/hsr/UI/element/" + key + ".webp"}
|
||||
alt={key}
|
||||
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md"
|
||||
width={200}
|
||||
height={200} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 lg:grid-cols-8 mb-1 mx-1 gap-2 overflow-y-auto w-full max-h-[17vh] min-h-[5vh]">
|
||||
{Object.entries(listPath).map(([key, value], index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setListPath((prev) => ({ ...prev, [key]: !prev[key] }))
|
||||
}}
|
||||
className="hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-lg cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
|
||||
}}
|
||||
>
|
||||
|
||||
<Image src={"https://api.hakush.in/hsr/UI/pathicon/" + key + ".webp"}
|
||||
alt={key}
|
||||
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] object-contain rounded-md"
|
||||
width={200}
|
||||
height={200} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start h-full">
|
||||
<ul className="grid grid-cols-1 md: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)}>
|
||||
<CharacterCard data={item} />
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
517
src/components/avatarInfo/index.tsx
Normal file
517
src/components/avatarInfo/index.tsx
Normal file
@@ -0,0 +1,517 @@
|
||||
"use client"
|
||||
|
||||
import { useRouter } from 'next/navigation'
|
||||
import useAvatarStore from "@/stores/avatarStore"
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import LightconeBar from '../lightconeBar'
|
||||
import useLightconeStore from '@/stores/lightconeStore'
|
||||
import { calcPromotion, calcRarity, replaceByParam } from '@/helper';
|
||||
import { getSkillTree } from '@/helper/getSkillTree';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import ParseText from '../parseText';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import useModelStore from '@/stores/modelStore';
|
||||
import useMazeStore from '@/stores/mazeStore';
|
||||
|
||||
export default function AvatarInfo() {
|
||||
const router = useRouter()
|
||||
const { avatarSelected, mapAvatarInfo } = useAvatarStore()
|
||||
const { Technique } = useMazeStore()
|
||||
const { avatars, setAvatars, setAvatar } = useUserDataStore()
|
||||
const { isOpenLightcone, setIsOpenLightcone } = useModelStore()
|
||||
const { listLightcone, mapLightconeInfo } = useLightconeStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { locale } = useLocaleStore();
|
||||
const lightcone = useMemo(() => {
|
||||
if (!avatarSelected) return null;
|
||||
const avatar = avatars[avatarSelected.id];
|
||||
return avatar?.profileList[avatar.profileSelect]?.lightcone || null;
|
||||
}, [avatarSelected, avatars]);
|
||||
|
||||
const lightconeDetail = useMemo(() => {
|
||||
if (!lightcone) return null;
|
||||
return listLightcone.find((item) => Number(item.id) === Number(lightcone.item_id)) || null;
|
||||
}, [lightcone, listLightcone]);
|
||||
|
||||
|
||||
|
||||
const handleShow = (modalId: string) => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
setIsOpenLightcone(true);
|
||||
modal.showModal();
|
||||
}
|
||||
};
|
||||
|
||||
// Close modal handler
|
||||
const handleCloseModal = (modalId: string) => {
|
||||
setIsOpenLightcone(false);
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
modal.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle ESC key to close modal
|
||||
useEffect(() => {
|
||||
if (!isOpenLightcone) {
|
||||
handleCloseModal("action_detail_modal");
|
||||
return;
|
||||
}
|
||||
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isOpenLightcone) {
|
||||
handleCloseModal("action_detail_modal");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleEscKey);
|
||||
|
||||
return () => window.removeEventListener('keydown', handleEscKey);
|
||||
}, [isOpenLightcone]);
|
||||
|
||||
return (
|
||||
<div className="bg-base-100 max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
|
||||
{avatarSelected && avatars[avatarSelected?.id || ""] && (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 w-full">
|
||||
<div className="m-2 min-h-96">
|
||||
<div className="container">
|
||||
<div className="card bg-base-200 shadow-xl">
|
||||
<div className="card-body">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="card-title text-2xl font-bold flex items-center gap-2">
|
||||
<div className="w-2 h-8 bg-gradient-to-b from-success to-success/50 rounded-full"></div>
|
||||
{transI18n("characterSettings")}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Level Control */}
|
||||
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10">
|
||||
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<div className="w-2 h-6 bg-gradient-to-b from-info to-info/50 rounded-full"></div>
|
||||
{transI18n("levelConfiguration")}
|
||||
</h4>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">{transI18n("characterLevel")}</span>
|
||||
<span className="label-text-alt text-info font-mono">{avatars[avatarSelected?.id || ""]?.level}/80</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="80"
|
||||
value={avatars[avatarSelected?.id || ""]?.level}
|
||||
onChange={(e) => {
|
||||
const newLevel = Math.min(80, Math.max(1, parseInt(e.target.value) || 1));
|
||||
|
||||
setAvatars({ ...avatars, [avatarSelected?.id || ""]: { ...avatars[avatarSelected?.id || ""], level: newLevel, promotion: calcPromotion(newLevel) } });
|
||||
}}
|
||||
className="input input-bordered w-full pr-16 font-mono"
|
||||
placeholder={transI18n("placeholderLevel")}
|
||||
/>
|
||||
<div
|
||||
onClick={() => {
|
||||
setAvatars({ ...avatars, [avatarSelected?.id || ""]: { ...avatars[avatarSelected?.id || ""], level: 80, promotion: calcPromotion(80) } });
|
||||
}}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-base-content/60 cursor-pointer">
|
||||
<span className="text-sm">{transI18n("max")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="80"
|
||||
value={avatars[avatarSelected?.id || ""]?.level}
|
||||
onChange={(e) => {
|
||||
const newLevel = Math.min(80, Math.max(1, parseInt(e.target.value) || 1));
|
||||
setAvatars({ ...avatars, [avatarSelected?.id || ""]: { ...avatars[avatarSelected?.id || ""], level: newLevel, promotion: calcPromotion(newLevel) } });
|
||||
}}
|
||||
className="range range-info range-sm w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Energy Control */}
|
||||
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10">
|
||||
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<div className="w-2 h-6 bg-gradient-to-b from-warning to-warning/50 rounded-full"></div>
|
||||
{transI18n("ultimateEnergy")}
|
||||
</h4>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">{transI18n("currentEnergy")}</span>
|
||||
<span className="label-text text-warning font-mono">
|
||||
{Math.round(avatars[avatarSelected?.id || ""]?.sp_value)}/{avatars[avatarSelected?.id || ""]?.sp_max}
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={avatars[avatarSelected?.id || ""]?.sp_max}
|
||||
value={avatars[avatarSelected?.id || ""]?.sp_value}
|
||||
onChange={(e) => {
|
||||
if (!avatars[avatarSelected?.id || ""]?.can_change_sp) return
|
||||
const newSpValue = Math.min(avatars[avatarSelected?.id || ""]?.sp_max, Math.max(0, parseInt(e.target.value) || 0));
|
||||
setAvatars({ ...avatars, [avatarSelected?.id || ""]: { ...avatars[avatarSelected?.id || ""], sp_value: newSpValue } });
|
||||
}}
|
||||
className="range range-warning range-sm w-full"
|
||||
/>
|
||||
<div className="flex justify-between text-sm text-base-content/60 mt-1">
|
||||
<span>0%</span>
|
||||
<span className="font-mono text-warning">{((avatars[avatarSelected?.id || ""]?.sp_value / avatars[avatarSelected?.id || ""]?.sp_max) * 100).toFixed(1)}%</span>
|
||||
<span>100%</span>
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<button
|
||||
className="btn btn-sm btn-outline btn-warning"
|
||||
onClick={() => {
|
||||
const newSpValue = Math.ceil(avatars[avatarSelected?.id || ""]?.sp_max / 2);
|
||||
const newAvatar = { ...avatars[avatarSelected?.id || ""], sp_value: newSpValue }
|
||||
console.log(newAvatar)
|
||||
setAvatar(newAvatar)
|
||||
}}
|
||||
>
|
||||
{transI18n("setTo50")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Technique Toggle */}
|
||||
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10">
|
||||
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<div className="w-2 h-6 bg-gradient-to-b from-accent to-accent/50 rounded-full"></div>
|
||||
{transI18n("battleConfiguration")}
|
||||
</h4>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="grid grid-cols-1 gap-2 sm:grid-cols-2 label cursor-pointer">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<span className="label-text font-medium">{transI18n("useTechnique")}</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={avatars[avatarSelected?.id || ""]?.techniques.length > 0}
|
||||
onChange={(e) => {
|
||||
const techniques = e.target.checked ? Technique[avatarSelected?.id || ""]?.maze_buff : [];
|
||||
const newAvatar = { ...avatars[avatarSelected?.id || ""], techniques };
|
||||
setAvatar(newAvatar);
|
||||
}}
|
||||
className="toggle toggle-accent"
|
||||
/>
|
||||
</label>
|
||||
<div className="text-xs text-base-content/60 mt-1">
|
||||
{transI18n("techniqueNote")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enhancement Selection */}
|
||||
{Object.entries(mapAvatarInfo[avatarSelected?.id || ""]?.Enhanced || {}).length > 0 && (
|
||||
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10">
|
||||
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-secondary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 0 0 4.486-6.336l-3.276 3.277a3.004 3.004 0 0 1-2.25-2.25l3.276-3.276a4.5 4.5 0 0 0-6.336 4.486c.091 1.076-.071 2.264-.904 2.95l-.102.085m-1.745 1.437L5.909 7.5H4.5L2.25 3.75l1.5-1.5L7.5 4.5v1.409l4.26 4.26m-1.745 1.437 1.745-1.437m6.615 8.206L15.75 15.75M4.867 19.125h.008v.008h-.008v-.008Z" />
|
||||
</svg>
|
||||
{transI18n("enhancement")}
|
||||
</h4>
|
||||
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">{transI18n("enhancementLevel")}</span>
|
||||
<span className="label-text-alt text-secondary font-mono">
|
||||
{avatars[avatarSelected?.id || ""]?.enhanced || transI18n("origin")}
|
||||
</span>
|
||||
</label>
|
||||
<select
|
||||
value={avatars[avatarSelected?.id || ""]?.enhanced || ""}
|
||||
onChange={(e) => {
|
||||
const newAvatar = avatars[avatarSelected?.id || ""]
|
||||
if (newAvatar) {
|
||||
newAvatar.enhanced = e.target.value
|
||||
const skillTree = getSkillTree(e.target.value)
|
||||
if (skillTree) {
|
||||
newAvatar.data.skills = skillTree
|
||||
}
|
||||
setAvatar(newAvatar)
|
||||
}
|
||||
}}
|
||||
className="select select-bordered select-secondary"
|
||||
>
|
||||
<option value="">{transI18n("origin")}</option>
|
||||
{Object.entries(mapAvatarInfo[avatarSelected?.id || ""]?.Enhanced || {}).map(([key, value]) => (
|
||||
<option key={key} value={key}>
|
||||
{key}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="text-xs text-base-content/60 mt-1">
|
||||
{transI18n("enhancedNote")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className="m-2 min-h-96">
|
||||
<div className="container">
|
||||
<div className="card bg-base-200 shadow-xl">
|
||||
<div className="card-body">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="card-title text-2xl font-bold flex items-center gap-2">
|
||||
<div className="w-2 h-8 bg-gradient-to-b from-primary to-secondary rounded-full"></div>
|
||||
{transI18n("lightconeEquipment")}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{lightcone && lightconeDetail ? (
|
||||
<div className="grid lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-3">
|
||||
{/* Level & Rank Controls */}
|
||||
<div className="bg-base-100 rounded-xl p-6 border border-base-content/10">
|
||||
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<div className="w-2 h-6 bg-gradient-to-b from-accent to-accent/50 rounded-full"></div>
|
||||
{transI18n("lightconeSettings")}
|
||||
</h4>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{/* Level Input */}
|
||||
<div className="form-control">
|
||||
<label className="label">
|
||||
<span className="label-text font-medium">{transI18n("level")}</span>
|
||||
<span className="label-text-alt text-primary font-mono">{lightcone.level}/80</span>
|
||||
</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max="80"
|
||||
value={lightcone.level || 1}
|
||||
onChange={(e) => {
|
||||
const newLightconeLevel = Math.min(80, Math.max(1, parseInt(e.target.value) || 1))
|
||||
const newLightcone = { ...lightcone, level: newLightconeLevel, promotion: calcPromotion(newLightconeLevel) }
|
||||
const newAvatar = { ...avatars[avatarSelected.id] }
|
||||
newAvatar.profileList[newAvatar.profileSelect].lightcone = newLightcone
|
||||
setAvatar(newAvatar)
|
||||
}}
|
||||
className="input input-bordered w-full pr-16 font-mono"
|
||||
placeholder={transI18n("placeholderLevel")}
|
||||
/>
|
||||
<div
|
||||
onClick={() => {
|
||||
const newLightcone = { ...lightcone, level: 80, promotion: calcPromotion(80) }
|
||||
const newAvatar = { ...avatars[avatarSelected.id] }
|
||||
newAvatar.profileList[newAvatar.profileSelect].lightcone = newLightcone
|
||||
setAvatar(newAvatar)
|
||||
}}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-base-content/60 cursor-pointer">
|
||||
<span className="text-sm">{transI18n("max")}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="80"
|
||||
value={lightcone.level || 1}
|
||||
onChange={(e) => {
|
||||
const newLightconeLevel = Math.min(80, Math.max(1, parseInt(e.target.value) || 1))
|
||||
const newLightcone = { ...lightcone, level: newLightconeLevel, promotion: calcPromotion(newLightconeLevel) }
|
||||
const newAvatar = { ...avatars[avatarSelected.id] }
|
||||
newAvatar.profileList[newAvatar.profileSelect].lightcone = newLightcone
|
||||
setAvatar(newAvatar)
|
||||
}}
|
||||
className="range range-primary range-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rank Selection */}
|
||||
<div className="form-control space-y-3">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||
<span className="font-medium text-sm sm:text-base">{transI18n("superimpositionRank")}</span>
|
||||
<span className="text-info font-mono text-lg sm:text-xl font-semibold">S{lightcone.rank}</span>
|
||||
</div>
|
||||
|
||||
{/* Rank Buttons */}
|
||||
<div className="w-full">
|
||||
<div className="grid grid-cols-5 gap-1.5 sm:gap-2 md:grid-cols-3 xl:grid-cols-5 max-w-sm mx-auto sm:mx-0">
|
||||
{[1, 2, 3, 4, 5].map((r) => (
|
||||
<button
|
||||
key={r}
|
||||
onClick={() => {
|
||||
const newLightcone = { ...lightcone, rank: r }
|
||||
const newAvatar = { ...avatars[avatarSelected.id] }
|
||||
newAvatar.profileList[newAvatar.profileSelect].lightcone = newLightcone
|
||||
setAvatar(newAvatar)
|
||||
}}
|
||||
className={`
|
||||
btn btn-sm sm:btn-md font-mono font-semibold
|
||||
transition-all duration-200 hover:scale-105
|
||||
${lightcone.rank === r
|
||||
? 'btn-primary shadow-lg'
|
||||
: 'btn-outline btn-primary hover:btn-primary'
|
||||
}`}
|
||||
>
|
||||
S{r}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Help Text */}
|
||||
<div className="text-xs sm:text-sm text-base-content/60 text-center sm:text-left mt-2">
|
||||
{transI18n("ranksNote")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex flex-wrap gap-3 mt-2">
|
||||
<button
|
||||
onClick={() => handleShow("action_detail_modal")}
|
||||
className="btn btn-primary btn-lg flex-1 min-w-fit"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" />
|
||||
</svg>
|
||||
{transI18n("changeLightcone")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const newAvatar = { ...avatars[avatarSelected.id] }
|
||||
newAvatar.profileList[newAvatar.profileSelect].lightcone = null
|
||||
setAvatar(newAvatar)
|
||||
}}
|
||||
className="btn btn-warning btn-lg flex-1 min-w-fit"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
{transI18n("removeLightcone")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lightcone Image */}
|
||||
<div className="lg:col-span-1">
|
||||
<div className="">
|
||||
<img
|
||||
loading="lazy"
|
||||
src={`https://api.hakush.in/hsr/UI/lightconemaxfigures/${lightconeDetail.id}.webp`}
|
||||
className="w-full h-full rounded-lg object-cover shadow-lg"
|
||||
alt="Lightcone"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lightcone Info & Controls */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Basic Info */}
|
||||
{mapLightconeInfo[lightcone.item_id] && (
|
||||
<div className="bg-base-300 rounded-xl p-6 border border-base-content/10">
|
||||
<div className="flex flex-wrap items-center gap-3 mb-4">
|
||||
<h3 className="text-2xl font-bold text-base-content">
|
||||
<ParseText
|
||||
locale={locale}
|
||||
text={mapLightconeInfo[lightcone.item_id].Name}
|
||||
/>
|
||||
</h3>
|
||||
<div className="badge badge-outline badge-lg">
|
||||
{transI18n(mapLightconeInfo[lightcone.item_id].BaseType.toLowerCase())}
|
||||
</div>
|
||||
<div className="badge badge-outline badge-lg">
|
||||
{calcRarity(mapLightconeInfo[lightcone.item_id].Rarity) + "⭐"}
|
||||
</div>
|
||||
<div className="badge badge-outline badge-lg">
|
||||
{"id: " + lightcone.item_id}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="prose prose-sm max-w-none">
|
||||
<div
|
||||
className="text-base-content/80 leading-relaxed"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
mapLightconeInfo[lightcone.item_id].Refinements.Desc,
|
||||
mapLightconeInfo[lightcone.item_id].Refinements.Level[lightcone.rank.toString()]?.ParamList || []
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* No Lightcone Equipped State */
|
||||
<div className="text-center py-12">
|
||||
<div
|
||||
onClick={() => handleShow("action_detail_modal")}
|
||||
className="w-24 h-24 mx-auto mb-6 bg-base-300 rounded-full flex items-center justify-center cursor-pointer"
|
||||
>
|
||||
<svg className="w-12 h-12 text-base-content/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2">{transI18n("noLightconeEquipped")}</h3>
|
||||
<p className="text-base-content/60 mb-6">{transI18n("equipLightconeNote")}</p>
|
||||
<button
|
||||
onClick={() => handleShow("action_detail_modal")}
|
||||
className="btn btn-success btn-lg"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
|
||||
</svg>
|
||||
{transI18n("equipLightcone")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<dialog id="action_detail_modal" className="modal backdrop-blur-sm">
|
||||
<div className="modal-box w-11/12 max-w-5xl 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={() => handleCloseModal("action_detail_modal")}
|
||||
>
|
||||
✕
|
||||
</motion.button>
|
||||
</div>
|
||||
<LightconeBar />
|
||||
</div>
|
||||
|
||||
</dialog>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
67
src/components/card/characterCard.tsx
Normal file
67
src/components/card/characterCard.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import { getNameChar } from '@/helper';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import { CharacterBasic } from '@/types';
|
||||
import ParseText from '../parseText';
|
||||
import Image from 'next/image';
|
||||
|
||||
interface CharacterCardProps {
|
||||
data: CharacterBasic
|
||||
}
|
||||
|
||||
export default function CharacterCard({ data }: CharacterCardProps) {
|
||||
const { locale } = useLocaleStore();
|
||||
const text = getNameChar(locale, data)
|
||||
return (
|
||||
<li
|
||||
className="z-10 flex flex-col items-center rounded-xl shadow-xl
|
||||
bg-gradient-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-[170px] sm:min-h-[180px] md:min-h-[210px] lg:min-h-[220px] xl:min-h-[240px] 2xl:min-h-[340px]"
|
||||
>
|
||||
<div
|
||||
className={`w-full rounded-md bg-gradient-to-br ${data.rank === "CombatPowerAvatarRarityType5"
|
||||
? "from-yellow-100 via-yellow-300 to-yellow-500"
|
||||
: "from-purple-100 via-purple-300 to-purple-500"
|
||||
}`}
|
||||
>
|
||||
|
||||
<div className="relative w-full h-full">
|
||||
<Image
|
||||
width={376}
|
||||
height={512}
|
||||
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${data.id}.webp`}
|
||||
className="w-full h-full rounded-md object-cover"
|
||||
alt="ALT"
|
||||
/>
|
||||
<Image
|
||||
width={32}
|
||||
height={32}
|
||||
|
||||
src={`https://api.hakush.in/hsr/UI/element/${data.damageType.toLowerCase()}.webp`}
|
||||
className="absolute top-0 left-0 w-6 h-6 rounded-full"
|
||||
alt={data.damageType.toLowerCase()}
|
||||
/>
|
||||
<Image
|
||||
width={32}
|
||||
height={32}
|
||||
src={`https://api.hakush.in/hsr/UI/pathicon/${data.baseType.toLowerCase()}.webp`}
|
||||
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={text}
|
||||
className="mt-2 px-1 text-center text-base font-normal leading-tight 2xl:text-lg"
|
||||
/>
|
||||
</li>
|
||||
|
||||
);
|
||||
}
|
||||
117
src/components/card/characterInfoCard.tsx
Normal file
117
src/components/card/characterInfoCard.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { CharacterInfoCardType } from '@/types';
|
||||
import { getNameChar, replaceByParam } from '@/helper';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import useAvatarStore from '@/stores/avatarStore';
|
||||
import useLightconeStore from '@/stores/lightconeStore';
|
||||
import useRelicStore from '@/stores/relicStore';
|
||||
import Image from 'next/image';
|
||||
import ParseText from '../parseText';
|
||||
|
||||
|
||||
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 { mapAvatarInfo } = useAvatarStore();
|
||||
const { mapLightconeInfo } = useLightconeStore();
|
||||
const { mapRelicInfo } = useRelicStore();
|
||||
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={`https://api.hakush.in/hsr/UI/avatarshopicon/${character.avatar_id}.webp`}
|
||||
alt={mapAvatarInfo[character.avatar_id.toString()]?.Name || ""}
|
||||
width={376}
|
||||
height={512}
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
src={`https://api.hakush.in/hsr/UI/element/${mapAvatarInfo[character.avatar_id.toString()]?.DamageType.toLowerCase()}.webp`}
|
||||
className="absolute top-0 left-0 w-10 h-10 rounded-full"
|
||||
alt={mapAvatarInfo[character.avatar_id.toString()]?.DamageType.toLowerCase()}
|
||||
/>
|
||||
<Image
|
||||
width={48}
|
||||
height={48}
|
||||
src={`https://api.hakush.in/hsr/UI/pathicon/${mapAvatarInfo[character.avatar_id.toString()]?.BaseType.toLowerCase()}.webp`}
|
||||
className="absolute top-0 right-0 w-10 h-10 rounded-full"
|
||||
alt={mapAvatarInfo[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={mapAvatarInfo[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={`https://api.hakush.in/hsr/UI/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.webp`}
|
||||
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>
|
||||
)}
|
||||
|
||||
{/* Light Cone */}
|
||||
{character.lightcone.item_id && (
|
||||
<div className="">
|
||||
<div className="rounded-lg h-42 flex items-center justify-center">
|
||||
<img
|
||||
src={`https://api.hakush.in/hsr/UI/lightconemediumicon/${character.lightcone.item_id}.webp`}
|
||||
alt={mapLightconeInfo[character.lightcone.item_id.toString()]?.Name}
|
||||
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={mapLightconeInfo[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>
|
||||
);
|
||||
};
|
||||
48
src/components/card/lightconeCard.tsx
Normal file
48
src/components/card/lightconeCard.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
"use client";
|
||||
|
||||
import { getNameLightcone } from '@/helper';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import { LightConeBasic } from '@/types';
|
||||
import ParseText from '../parseText';
|
||||
|
||||
interface LightconeCardProps {
|
||||
data: LightConeBasic
|
||||
}
|
||||
|
||||
export default function LightconeCard({ data }: LightconeCardProps) {
|
||||
|
||||
const { locale } = useLocaleStore();
|
||||
const text = getNameLightcone(locale, data)
|
||||
return (
|
||||
<li className="z-10 flex flex-col items-center rounded-md shadow-lg
|
||||
bg-gradient-to-b from-customStart to-customEnd transform transition-transform duration-300
|
||||
hover:scale-105 cursor-pointer min-h-[220px]"
|
||||
>
|
||||
<div
|
||||
className={`w-full rounded-md bg-gradient-to-br ${data.rank === "CombatPowerLightconeRarity5"
|
||||
? "from-yellow-400 via-yellow-100 to-yellow-500"
|
||||
: data.rank === "CombatPowerLightconeRarity4" ? "from-purple-300 via-purple-100 to-purple-400" :
|
||||
"from-blue-300 via-blue-100 to-blue-400"
|
||||
}`}
|
||||
>
|
||||
|
||||
<div className="relative w-full h-full">
|
||||
<img
|
||||
loading="lazy"
|
||||
src={`https://api.hakush.in/hsr/UI/lightconemediumicon/${data.id}.webp`}
|
||||
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-base font-normal leading-tight"
|
||||
/>
|
||||
</li>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
73
src/components/card/profileCard.tsx
Normal file
73
src/components/card/profileCard.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
|
||||
import React from 'react';
|
||||
import { AvatarProfileCardType } from '@/types';
|
||||
import useLocaleStore from '@/stores/localeStore';
|
||||
import useAvatarStore from '@/stores/avatarStore';
|
||||
import useLightconeStore from '@/stores/lightconeStore';
|
||||
import useRelicStore from '@/stores/relicStore';
|
||||
import Image from 'next/image';
|
||||
import ParseText from '../parseText';
|
||||
|
||||
|
||||
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 { mapLightconeInfo } = useLightconeStore();
|
||||
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">
|
||||
<img
|
||||
src={`https://api.hakush.in/hsr/UI/lightconemediumicon/${profile.lightcone.item_id}.webp`}
|
||||
alt={mapLightconeInfo[profile.lightcone.item_id.toString()]?.Name}
|
||||
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={mapLightconeInfo[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.entries(profile.relics).map(([key, 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={`https://api.hakush.in/hsr/UI/relicfigures/IconRelic_${relic.relic_set_id}_${relic.relic_id.toString()[relic.relic_id.toString().length - 1]}.webp`}
|
||||
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>
|
||||
);
|
||||
};
|
||||
143
src/components/card/relicCard.tsx
Normal file
143
src/components/card/relicCard.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
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-gradient-to-br from-green-700 via-green-400 to-green-500';
|
||||
case '4': return 'border-blue-500 shadow-blue-500/50 bg-gradient-to-br from-blue-700 via-blue-400 to-blue-500';
|
||||
case '5': return 'border-purple-500 shadow-purple-500/50 bg-gradient-to-br from-purple-700 via-purple-400 to-purple-500';
|
||||
case '6': return 'border-yellow-500 shadow-yellow-500/50 bg-gradient-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 src="/relics/HEAD.png" alt="Head" width={20} height={20} />
|
||||
<h2>Head</h2>
|
||||
</div>
|
||||
);
|
||||
case '2': return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Image src="/relics/HAND.png" alt="Head" width={20} height={20} />
|
||||
<h2>Hands</h2>
|
||||
</div>
|
||||
);
|
||||
case '3': return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Image src="/relics/BODY.png" alt="Head" width={20} height={20} />
|
||||
<h2>Body</h2>
|
||||
</div>
|
||||
);
|
||||
case '4': return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Image src="/relics/FOOT.png" alt="Head" width={20} height={20} />
|
||||
<h2>Feet</h2>
|
||||
</div>
|
||||
);
|
||||
case '5': return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Image src="/relics/OBJECT.png" alt="Head" width={20} height={20} />
|
||||
<h2>Planar sphere</h2>
|
||||
</div>
|
||||
);
|
||||
case '6': return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Image src="/relics/NECK.png" alt="Head" width={20} height={20} />
|
||||
<h2>Link rope</h2>
|
||||
</div>
|
||||
);
|
||||
default: return '';
|
||||
}
|
||||
};
|
||||
export default function RelicCard({ slot, avatarId }: RelicCardProps) {
|
||||
const { locale } = useLocaleStore();
|
||||
const { avatars } = useUserDataStore()
|
||||
|
||||
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 border-4
|
||||
${getRarityColor(relicDetail.relic_id.toString()[0])}
|
||||
shadow-xl
|
||||
flex items-center justify-center
|
||||
cursor-pointer hover:scale-105 transition-transform
|
||||
ring-4 ring-primary
|
||||
`}
|
||||
>
|
||||
<span>
|
||||
<Image
|
||||
src={`https://api.hakush.in/hsr/UI/relicfigures/IconRelic_${relicDetail.relic_set_id}_${slot}.webp`}
|
||||
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>
|
||||
)
|
||||
}
|
||||
665
src/components/card/showCaseCard.tsx
Normal file
665
src/components/card/showCaseCard.tsx
Normal file
@@ -0,0 +1,665 @@
|
||||
"use client";
|
||||
import React, { useState, useRef} from 'react';
|
||||
|
||||
export default function ShowCaseInfo() {
|
||||
const [imageUrl, setImageUrl] = useState<string>('https://api.hakush.in/hsr/UI/avatardrawcard/1001.webp');
|
||||
const [position, setPosition] = useState<{ x: number; y: number }>({ x: 0, y: 100 });
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-start m-2">
|
||||
|
||||
<div className="overflow-auto">
|
||||
<div ref={ref} className="relative min-h-[650px] w-[1400px] rounded-3xl overflow-hidden">
|
||||
<img
|
||||
src="https://cdn.jsdelivr.net/gh/picklejason/hsr-showcase@main/public/blur-bg.png"
|
||||
alt="Showcase Background"
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
/>
|
||||
<div className="absolute bottom-2 left-4 z-10">
|
||||
<span className="shadow-black [text-shadow:1px_1px_2px_var(--tw-shadow-color)]"></span>
|
||||
</div>
|
||||
<div className="flex flex-row items-center">
|
||||
<div
|
||||
className="relative min-h-[650px] w-[28%]"
|
||||
|
||||
>
|
||||
<div
|
||||
className="flex h-[650px] items-center"
|
||||
style={{ cursor: 'grab' }}
|
||||
>
|
||||
<div className="overflow-hidden w-full h-full"
|
||||
>
|
||||
<img
|
||||
src={imageUrl}
|
||||
className="scale-[1.8] cursor-pointer object-cover"
|
||||
alt="Character Preview"
|
||||
style={{
|
||||
position: 'relative',
|
||||
top: `${position.y}px`,
|
||||
left: `${position.x}px`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute right-0 top-0 mr-[-15px] pt-3">
|
||||
<div className="flex flex-col">
|
||||
{[
|
||||
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/memory-of-you-eidolon_icon_small.webp",
|
||||
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/memory-of-it-eidolon_icon_small.webp",
|
||||
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/memory-of-everything-eidolon_icon_small.webp",
|
||||
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/never-forfeit-again-eidolon_icon_small.webp",
|
||||
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/never-forget-again-eidolon_icon_small.webp",
|
||||
"https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/eidolon/just-like-this-always-eidolon_icon_small.webp"
|
||||
].map((src, index) => (
|
||||
<div key={index} className="relative my-1 flex rounded-full border-2 border-neutral-300 bg-neutral-800">
|
||||
<img src={src} alt="Rank Icon" className="h-auto w-10" />
|
||||
<div className="absolute flex h-full w-full items-center justify-center rounded-full bg-neutral-800/70">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" className="size-5">
|
||||
<path fillRule="evenodd" d="M10 1a4.5 4.5 0 0 0-4.5 4.5V9H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-.5V5.5A4.5 4.5 0 0 0 10 1Zm3 8V5.5a3 3 0 1 0-6 0V9h6Z" clipRule="evenodd"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex min-h-[650px] w-[72%] flex-row items-center gap-3.5 rounded-r-3xl pl-10">
|
||||
<img
|
||||
src="https://cdn.jsdelivr.net/gh/picklejason/hsr-showcase@main/public/fade-bg.png"
|
||||
alt="Background"
|
||||
className="absolute inset-0 w-full h-full object-cover rounded-r-3xl"
|
||||
style={{ zIndex: -1 }}
|
||||
/>
|
||||
|
||||
<div className="flex h-[650px] w-1/3 flex-col justify-between py-3">
|
||||
<div className="flex h-full flex-col justify-between">
|
||||
<div>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<span className="text-4xl">March 7th</span>
|
||||
<img src="https://api.hakush.in/hsr/UI/element/ice.webp" alt="Element Icon" className="h-auto w-14" />
|
||||
</div>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<img src="https://api.hakush.in/hsr/UI/pathicon/knight.webp" alt="Path Icon" className="h-auto w-8" />
|
||||
<span className="text-xl">Preservation</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-2xl">Lv. 80</span>
|
||||
<span className="text-xl"> / </span>
|
||||
<span className="text-xl text-neutral-400">80</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mx-4 flex h-[225px] w-auto flex-row items-center justify-evenly">
|
||||
<div className="absolute mb-5">
|
||||
<img src="https://api.hakush.in/hsr/UI/pathicon/knight.webp" alt="Path Icon" className="h-40 w-40 opacity-20" />
|
||||
</div>
|
||||
<div className="flex h-full w-1/3 flex-col justify-center gap-8">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="relative flex flex-col items-center">
|
||||
<img src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/skill/frigid-cold-arrow-skill_icon.webp" alt="Skill Icon" className="h-auto w-12 rounded-full border-2 border-neutral-500 bg-neutral-800" />
|
||||
<span className="black-blur absolute bottom-4 text-sm">6 / 6</span>
|
||||
<span className="z-10 mt-1.5 truncate text-sm">Basic ATK</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="relative flex flex-col items-center">
|
||||
<img src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/skill/the-power-of-cuteness-skill_icon.webp" alt="Skill Icon" className="h-auto w-12 rounded-full border-2 border-neutral-500 bg-neutral-800" />
|
||||
<span className="black-blur absolute bottom-4 text-sm">10 / 10</span>
|
||||
<span className="z-10 mt-1.5 truncate text-sm">Skill</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-1/3 justify-center">
|
||||
<div className="relative flex flex-col items-center">
|
||||
<img src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/skill/glacial-cascade-skill_icon.webp" alt="Skill Icon" className="h-auto w-12 rounded-full border-2 border-neutral-500 bg-neutral-800" />
|
||||
<span className="black-blur absolute bottom-4 text-sm">10 / 10</span>
|
||||
<span className="z-10 mt-1.5 truncate text-sm">Ultimate</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex h-full w-1/3 flex-col justify-center gap-8">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="relative flex flex-col items-center">
|
||||
<img src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/skill/girl-power-skill_icon.webp" alt="Skill Icon" className="h-auto w-12 rounded-full border-2 border-neutral-500 bg-neutral-800" />
|
||||
<span className="black-blur absolute bottom-4 text-sm">8 / 8</span>
|
||||
<span className="z-10 mt-1.5 truncate text-sm">Talent</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="relative flex flex-col items-center">
|
||||
<img src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/skill/freezing-beauty-skill_icon.webp" alt="Skill Icon" className="h-auto w-12 rounded-full border-2 border-neutral-500 bg-neutral-800" />
|
||||
<span className="black-blur absolute bottom-4 text-sm">5 / 5</span>
|
||||
<span className="z-10 mt-1.5 truncate text-sm">Technique</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="flex w-full flex-row justify-evenly">
|
||||
{/* First Column */}
|
||||
<div className="flex flex-col justify-center items-center" style={{ gap: '0.25rem' }}>
|
||||
<img
|
||||
src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/trace/purify-trace_icon.webp"
|
||||
alt="Icon 1001101"
|
||||
className="bg-neutral-800 h-auto w-5 rounded-full"
|
||||
/>
|
||||
<div className="flex flex-row justify-center items-center" style={{ gap: '0.25rem' }}>
|
||||
<img
|
||||
src="https://api.hakush.in/hsr/UI/trace/IconDefence.webp"
|
||||
alt="Icon 1001101"
|
||||
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||
/>
|
||||
<img
|
||||
src="https://api.hakush.in/hsr/UI/trace/IconIceAddedRatio.webp"
|
||||
alt="Icon 1001101"
|
||||
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Second Column */}
|
||||
<div className="flex flex-col justify-center items-center" style={{ gap: '0.25rem' }}>
|
||||
<img
|
||||
src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/trace/reinforce-trace_icon.webp"
|
||||
alt="Icon 1001102"
|
||||
className="bg-neutral-800 h-auto w-5 rounded-full"
|
||||
/>
|
||||
<div className="flex flex-row justify-center items-center" style={{ gap: '0.25rem' }}>
|
||||
<img
|
||||
src="https://api.hakush.in/hsr/UI/trace/IconIceAddedRatio.webp"
|
||||
alt="Icon 1001102"
|
||||
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||
/>
|
||||
<img
|
||||
src="https://api.hakush.in/hsr/UI/trace/IconStatusResistance.webp"
|
||||
alt="Icon 1001102"
|
||||
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Third Column */}
|
||||
<div className="flex flex-col justify-center items-center" style={{ gap: '0.25rem' }}>
|
||||
<img
|
||||
src="https://worker-sparkling-dawn-a1c1.srv2.workers.dev/hsr.honeyhunterworld.com/img/trace/ice-spell-trace_icon.webp"
|
||||
alt="Icon 1001103"
|
||||
className="bg-neutral-800 h-auto w-5 rounded-full"
|
||||
/>
|
||||
<div className="flex flex-row justify-center items-center" style={{ gap: '0.25rem' }}>
|
||||
<img
|
||||
src="https://api.hakush.in/hsr/UI/trace/IconIceAddedRatio.webp"
|
||||
alt="Icon 1001103"
|
||||
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||
/>
|
||||
<img
|
||||
src="https://api.hakush.in/hsr/UI/trace/IconIceAddedRatio.webp"
|
||||
alt="Icon 1001103"
|
||||
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||
/>
|
||||
<img
|
||||
src="https://api.hakush.in/hsr/UI/trace/IconDefence.webp"
|
||||
alt="Icon 1001103"
|
||||
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fourth Column */}
|
||||
<div className="flex flex-col justify-center items-center" style={{ gap: '0.25rem' }}>
|
||||
<div className="flex flex-row justify-center items-center" style={{ gap: '0.25rem' }}>
|
||||
<img
|
||||
src="https://api.hakush.in/hsr/UI/trace/IconIceAddedRatio.webp"
|
||||
alt="Icon 1001201"
|
||||
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||
/>
|
||||
<img
|
||||
src="https://api.hakush.in/hsr/UI/trace/IconStatusResistance.webp"
|
||||
alt="Icon 1001201"
|
||||
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||
/>
|
||||
<img
|
||||
src="https://api.hakush.in/hsr/UI/trace/IconDefence.webp"
|
||||
alt="Icon 1001201"
|
||||
className="bg-neutral-800 h-auto w-3 rounded-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<div className="relative flex flex-col items-center">
|
||||
<img
|
||||
src="https://api.hakush.in/hsr/UI/lightconemaxfigures/24000.webp"
|
||||
alt="Light Cone Preview"
|
||||
className="h-auto w-32"
|
||||
/>
|
||||
<img
|
||||
src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png"
|
||||
alt="Light Cone Rarity Icon"
|
||||
className="absolute bottom-0 left-1 h-auto w-36"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex w-3/5 flex-col items-center gap-2 text-center">
|
||||
<span className="text-xl">On the Fall of an Aeon</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-6 w-6 rounded-full font-normal bg-[#f6ce71] text-black">
|
||||
V
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-lg">Lv. 80</span>
|
||||
<span> / </span>
|
||||
<span className="text-neutral-400">80</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-1.5">
|
||||
<div className="black-blur flex flex-row items-center rounded pr-1">
|
||||
<img
|
||||
src="https://srtools.pages.dev/icon/property/IconAttack.png"
|
||||
alt="Attribute Icon"
|
||||
className="h-auto w-6 p-1"
|
||||
/>
|
||||
<span className="text-sm">529</span>
|
||||
</div>
|
||||
<div className="black-blur flex flex-row items-center rounded pr-1">
|
||||
<img
|
||||
src="https://srtools.pages.dev/icon/property/IconDefence.png"
|
||||
alt="Attribute Icon"
|
||||
className="h-auto w-6 p-1"
|
||||
/>
|
||||
<span className="text-sm">397</span>
|
||||
</div>
|
||||
<div className="black-blur flex flex-row items-center rounded pr-1">
|
||||
<img
|
||||
src="https://srtools.pages.dev/icon/property/IconMaxHP.png"
|
||||
alt="Attribute Icon"
|
||||
className="h-auto w-6 p-1"
|
||||
/>
|
||||
<span className="text-sm">1058</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-[650px] w-1/3 flex-col justify-between py-3">
|
||||
<div className="flex w-full flex-col justify-between gap-y-0.5 text-base h-[500px]">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||
<span className="font-bold">HP</span>
|
||||
</div>
|
||||
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||
<div className="flex cursor-default flex-col text-right font-bold">2942</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||
<span className="font-bold">ATK</span>
|
||||
</div>
|
||||
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||
<div className="flex cursor-default flex-col text-right font-bold">3212</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconDefence.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||
<span className="font-bold">DEF</span>
|
||||
</div>
|
||||
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||
<div className="flex cursor-default flex-col text-right font-bold">1255</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconSpeed.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||
<span className="font-bold">SPD</span>
|
||||
</div>
|
||||
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||
<div className="flex cursor-default flex-col text-right font-bold">106</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||
<span className="font-bold">CRIT Rate</span>
|
||||
</div>
|
||||
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||
<div className="flex cursor-default flex-col text-right font-bold">16.7%</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||
<span className="font-bold">CRIT DMG</span>
|
||||
</div>
|
||||
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||
<div className="flex cursor-default flex-col text-right font-bold">79.2%</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconBreakUp.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||
<span className="font-bold">Break Effect</span>
|
||||
</div>
|
||||
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||
<div className="flex cursor-default flex-col text-right font-bold">186.9%</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconStatusResistance.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||
<span className="font-bold">Effect RES</span>
|
||||
</div>
|
||||
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||
<div className="flex cursor-default flex-col text-right font-bold">4.0%</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconEnergyRecovery.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||
<span className="font-bold">Energy Regeneration Rate</span>
|
||||
</div>
|
||||
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||
<div className="flex cursor-default flex-col text-right font-bold">0.0%</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconStatusProbability.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||
<span className="font-bold">Effect Hit Rate</span>
|
||||
</div>
|
||||
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||
<div className="flex cursor-default flex-col text-right font-bold">20.3%</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconHealRatio.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||
<span className="font-bold">Outgoing Healing Boost</span>
|
||||
</div>
|
||||
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||
<div className="flex cursor-default flex-col text-right font-bold">0.0%</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconIceAddedRatio.png" alt="Stat Icon" className="h-auto w-10 p-2" />
|
||||
<span className="font-bold">Ice DMG Boost</span>
|
||||
</div>
|
||||
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
|
||||
<div className="flex cursor-default flex-col text-right font-bold">6.4%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
<div className="flex w-full flex-row justify-between text-left">
|
||||
<span>Prisoner in Deep Confinement</span>
|
||||
<div>
|
||||
<span className="black-blur bg-black flex w-5 justify-center rounded px-1.5 py-0.5">2</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-row justify-between text-left">
|
||||
<span>Watchmaker, Master of Dream Machinations</span>
|
||||
<div>
|
||||
<span className="black-blur bg-black flex w-5 justify-center rounded px-1.5 py-0.5">2</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-row justify-between text-left">
|
||||
<span>Talia: Kingdom of Banditry</span>
|
||||
<div>
|
||||
<span className="black-blur bg-black flex w-5 justify-center rounded px-1.5 py-0.5">2</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-1/3">
|
||||
<div className="flex h-[650px] flex-col justify-between py-3 text-lg">
|
||||
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
|
||||
<div className="flex">
|
||||
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_116_1.webp" alt="Relic Icon" className="h-auto w-20" />
|
||||
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
|
||||
</div>
|
||||
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Main Affix Icon" className="h-auto w-9" />
|
||||
<span className="text-base text-[#f1a23c]">705</span>
|
||||
<span className="black-blur rounded px-1 text-xs">+15</span>
|
||||
</div>
|
||||
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
|
||||
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+7.8%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+3.2%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+17.5%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconBreakUp.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+15.6%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
|
||||
<div className="flex">
|
||||
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_118_2.webp" alt="Relic Icon" className="h-auto w-20" />
|
||||
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
|
||||
</div>
|
||||
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Main Affix Icon" className="h-auto w-9" />
|
||||
<span className="text-base text-[#f1a23c]">352</span>
|
||||
<span className="black-blur rounded px-1 text-xs">+15</span>
|
||||
</div>
|
||||
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
|
||||
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+6.9%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+11.7%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconStatusProbability.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+3.9%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconBreakUp.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+23.3%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
|
||||
<div className="flex">
|
||||
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_116_3.webp" alt="Relic Icon" className="h-auto w-20" />
|
||||
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
|
||||
</div>
|
||||
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Main Affix Icon" className="h-auto w-9" />
|
||||
<span className="text-base text-[#f1a23c]">43.2%</span>
|
||||
<span className="black-blur rounded px-1 text-xs">+15</span>
|
||||
</div>
|
||||
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
|
||||
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconDefence.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+57</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+3.9%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+2.6%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconBreakUp.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+19.4%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
|
||||
<div className="flex">
|
||||
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_118_4.webp" alt="Relic Icon" className="h-auto w-20" />
|
||||
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
|
||||
</div>
|
||||
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Main Affix Icon" className="h-auto w-9" />
|
||||
<span className="text-base text-[#f1a23c]">18.9%</span>
|
||||
<span className="black-blur rounded px-1 text-xs">+15</span>
|
||||
</div>
|
||||
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
|
||||
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+5.2%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+4.3%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+2.7%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconDefence.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+34</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
|
||||
<div className="flex">
|
||||
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_118_4.webp" alt="Relic Icon" className="h-auto w-20" />
|
||||
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
|
||||
</div>
|
||||
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Main Affix Icon" className="h-auto w-9" />
|
||||
<span className="text-base text-[#f1a23c]">18.9%</span>
|
||||
<span className="black-blur rounded px-1 text-xs">+15</span>
|
||||
</div>
|
||||
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
|
||||
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+5.2%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+4.3%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+2.7%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconDefence.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+34</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600">
|
||||
<div className="flex">
|
||||
<img src="https://api.hakush.in/hsr/UI/relicfigures/IconRelic_118_4.webp" alt="Relic Icon" className="h-auto w-20" />
|
||||
<img src="https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/deco/Star5.png" alt="Relic Rarity Icon" className="absolute bottom-1 h-auto w-20" />
|
||||
</div>
|
||||
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconAttack.png" alt="Main Affix Icon" className="h-auto w-9" />
|
||||
<span className="text-base text-[#f1a23c]">18.9%</span>
|
||||
<span className="black-blur rounded px-1 text-xs">+15</span>
|
||||
</div>
|
||||
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div>
|
||||
<div className="m-auto grid w-1/2 grid-cols-2 gap-2">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconMaxHP.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+5.2%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconCriticalDamage.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+4.3%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconCriticalChance.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+2.7%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-row items-center">
|
||||
<img src="https://srtools.pages.dev/icon/property/IconDefence.png" alt="Sub Affix Icon" className="h-auto w-7" />
|
||||
<span className="text-sm">+34</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
66
src/components/eidolonsInfo/index.tsx
Normal file
66
src/components/eidolonsInfo/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client"
|
||||
import { replaceByParam } from "@/helper";
|
||||
import useListAvatarStore from "@/stores/avatarStore";
|
||||
import Image from "next/image";
|
||||
import ParseText from "../parseText";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export default function EidolonsInfo() {
|
||||
const { avatarSelected, mapAvatarInfo } = useListAvatarStore()
|
||||
const { locale } = useLocaleStore()
|
||||
const { setAvatars, avatars } = useUserDataStore()
|
||||
|
||||
const charRank = useMemo(() => {
|
||||
if (!avatarSelected) return null;
|
||||
const avatar = avatars[avatarSelected.id];
|
||||
if (avatar?.enhanced != "") {
|
||||
return mapAvatarInfo[avatarSelected.id]?.Enhanced[avatar?.enhanced].Ranks
|
||||
}
|
||||
return mapAvatarInfo[avatarSelected.id]?.Ranks
|
||||
}, [avatarSelected, avatars, locale, mapAvatarInfo]);
|
||||
|
||||
return (
|
||||
<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 cursor-pointer hover:scale-105"
|
||||
onClick={() => {
|
||||
let newRank = Number(key)
|
||||
if (Number(key) == 1 && avatars[avatarSelected?.id || ""]?.data?.rank == 1) {
|
||||
newRank = 0
|
||||
}
|
||||
setAvatars({ ...avatars, [avatarSelected?.id || ""]: { ...avatars[avatarSelected?.id || ""], data: { ...avatars[avatarSelected?.id || ""].data, rank: newRank } } })
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
|
||||
loading="lazy"
|
||||
className={`w-60 h-60 mb-2 ${Number(key) <= avatars[avatarSelected?.id || ""]?.data?.rank ? "" : "grayscale"}`}
|
||||
src={`https://api.hakush.in/hsr/UI/rank/_dependencies/textures/${avatarSelected?.id}/${avatarSelected?.id}_Rank_${key}.webp`}
|
||||
alt={`Rank ${key}`}
|
||||
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">{key}.</span>
|
||||
<ParseText
|
||||
locale={locale}
|
||||
text={rank.Name}
|
||||
className="text-center text-base font-normal leading-tight"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm font-normal">
|
||||
<div dangerouslySetInnerHTML={{ __html: replaceByParam(rank.Desc, rank.ParamList) }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
src/components/footer/index.tsx
Normal file
11
src/components/footer/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
|
||||
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()} - Kain (Powered by Nextjs & DaisyUi)</p>
|
||||
</aside>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
602
src/components/header/index.tsx
Normal file
602
src/components/header/index.tsx
Normal file
@@ -0,0 +1,602 @@
|
||||
"use client"
|
||||
import { connectToPS, downloadJson, syncDataToPS } from "@/helper";
|
||||
import { converterToFreeSRJson } from "@/helper/converterToFreeSRJson";
|
||||
import { useChangeTheme } from "@/hooks/useChangeTheme";
|
||||
|
||||
import { listCurrentLanguage } from "@/lib/constant";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { motion } from "framer-motion";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import EnkaImport from "../importBar/enka";
|
||||
import useModelStore from "@/stores/modelStore";
|
||||
import FreeSRImport from "../importBar/freesr";
|
||||
import { toast } from "react-toastify";
|
||||
import { micsSchema } from "@/zod";
|
||||
import useConnectStore from "@/stores/connectStore";
|
||||
import useGlobalStore from "@/stores/globalStore";
|
||||
|
||||
const themes = [
|
||||
{ label: "Winter" },
|
||||
{ label: "Night" },
|
||||
{ label: "Cupcake" },
|
||||
{ label: "Coffee" },
|
||||
];
|
||||
|
||||
export default function Header() {
|
||||
const { changeTheme } = useChangeTheme()
|
||||
const { locale, setLocale } = useLocaleStore()
|
||||
const { avatars, battle_config, setAvatars, setBattleConfig } = useUserDataStore()
|
||||
|
||||
const router = useRouter()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { setIsOpenImport, isOpenImport } = useModelStore()
|
||||
|
||||
const [message, setMessage] = useState({ text: '', type: '' });
|
||||
const [importModal, setImportModal] = useState("enka");
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const {
|
||||
connectionType,
|
||||
privateType,
|
||||
serverUrl,
|
||||
username,
|
||||
password,
|
||||
setConnectionType,
|
||||
setPrivateType,
|
||||
setServerUrl,
|
||||
setUsername,
|
||||
setPassword
|
||||
} = useConnectStore()
|
||||
const { isConnectPS, setIsConnectPS } = useGlobalStore()
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const cookieLocale = document.cookie.split("; ")
|
||||
.find((row) => row.startsWith("MYNEXTAPP_LOCALE"))
|
||||
?.split("=")[1];
|
||||
|
||||
if (cookieLocale) {
|
||||
if (!listCurrentLanguage.hasOwnProperty(cookieLocale)) {
|
||||
setLocale("en")
|
||||
} else {
|
||||
setLocale(cookieLocale)
|
||||
}
|
||||
|
||||
} else {
|
||||
let browserLocale = navigator.language.slice(0, 2);
|
||||
|
||||
if (!listCurrentLanguage.hasOwnProperty(browserLocale)) {
|
||||
browserLocale = "en"
|
||||
}
|
||||
setLocale(browserLocale);
|
||||
document.cookie = `MYNEXTAPP_LOCALE=${browserLocale};`
|
||||
router.refresh()
|
||||
}
|
||||
}, [router, setLocale])
|
||||
|
||||
const changeLocale = (newLocale: string) => {
|
||||
setLocale(newLocale)
|
||||
document.cookie = `MYNEXTAPP_LOCALE=${newLocale};`
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
const handleShow = (modalId: string) => {
|
||||
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
setIsModalOpen(true);
|
||||
modal.showModal();
|
||||
}
|
||||
};
|
||||
|
||||
// Close modal handler
|
||||
const handleCloseModal = (modalId: string) => {
|
||||
setIsModalOpen(false);
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
modal.close()
|
||||
}
|
||||
};
|
||||
|
||||
// Handle ESC key to close modal
|
||||
useEffect(() => {
|
||||
if (!isOpenImport) {
|
||||
handleCloseModal("import_modal");
|
||||
return;
|
||||
}
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
handleCloseModal("import_modal");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleEscKey);
|
||||
return () => window.removeEventListener('keydown', handleEscKey);
|
||||
}, [isModalOpen, isOpenImport]);
|
||||
|
||||
const handleImportDatabase = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) {
|
||||
toast.error(transI18n("pleaseSelectAFile"))
|
||||
|
||||
return
|
||||
}
|
||||
if (!file.name.endsWith(".json") || file.type !== "application/json") {
|
||||
toast.error(transI18n("fileMustBeAValidJsonFile"))
|
||||
return
|
||||
}
|
||||
|
||||
if (file) {
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.target?.result as string);
|
||||
const parsed = micsSchema.parse(data)
|
||||
setAvatars(parsed.avatars)
|
||||
setBattleConfig(parsed.battle_config)
|
||||
toast.success(transI18n("importDatabaseSuccess"))
|
||||
} catch (error) {
|
||||
toast.error(transI18n("fileMustBeAValidJsonFile"))
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="navbar bg-base-100 shadow-md sticky top-0 z-50 px-3 py-1">
|
||||
<div className="navbar-start">
|
||||
{/* Mobile menu dropdown */}
|
||||
<div className="dropdown">
|
||||
<div tabIndex={0} role="button" className="btn btn-ghost btn-sm lg:hidden hover:bg-base-200 transition-all duration-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h8m-8 6h16" />
|
||||
</svg>
|
||||
</div>
|
||||
<ul
|
||||
tabIndex={0}
|
||||
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-10 mt-3 w-52 p-2 shadow-md border border-base-200"
|
||||
>
|
||||
<li>
|
||||
<details>
|
||||
<summary className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium w-full">{transI18n("loadData")}</summary>
|
||||
<ul className="p-2">
|
||||
<li><a onClick={() => {
|
||||
setImportModal("freesr")
|
||||
setIsOpenImport(true)
|
||||
handleShow("import_modal")
|
||||
}}>{transI18n("freeSr")}</a></li>
|
||||
<li>
|
||||
<>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/json"
|
||||
id="database-data-upload"
|
||||
className="hidden"
|
||||
onChange={handleImportDatabase}
|
||||
/>
|
||||
<button
|
||||
onClick={() => document.getElementById('database-data-upload')?.click()}
|
||||
>
|
||||
{transI18n("database")}
|
||||
</button>
|
||||
</>
|
||||
</li>
|
||||
<li><a onClick={() => {
|
||||
setImportModal("enka")
|
||||
setIsOpenImport(true)
|
||||
handleShow("import_modal")
|
||||
}}>{transI18n("enka")}</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
<details>
|
||||
<summary className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium">{transI18n("exportData")}</summary>
|
||||
<ul className="p-2">
|
||||
<li><a onClick={() => downloadJson("freesr-data", converterToFreeSRJson(avatars, battle_config))}>{transI18n("freeSr")}</a></li>
|
||||
<li><a onClick={() => downloadJson("database-data", { avatars: avatars, battle_config: battle_config })}>{transI18n("database")}</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||
onClick={() => handleShow("connection_modal")}
|
||||
>
|
||||
{transI18n("connectSetting")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
disabled
|
||||
className="disabled px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||
>
|
||||
{transI18n("monsterSetting")} ({transI18n("comingSoon")})
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* Logo */}
|
||||
|
||||
<a className="hidden sm:grid sm:grid-cols-1 items-start text-left gap-0 hover:scale-105 px-2">
|
||||
<h1 className="text-xl font-bold">
|
||||
<span className="text-emerald-500">Firefly Sr</span>
|
||||
<span className="bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 via-orange-500 to-red-500">
|
||||
Tools
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">By Kain</p>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Desktop navigation */}
|
||||
<div className="navbar-center hidden lg:flex">
|
||||
<ul className="menu menu-horizontal gap-1">
|
||||
<li>
|
||||
<details>
|
||||
<summary className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium">{transI18n("loadData")}</summary>
|
||||
<ul className="p-2">
|
||||
<li><a onClick={() => {
|
||||
setImportModal("freesr")
|
||||
setIsOpenImport(true)
|
||||
handleShow("import_modal")
|
||||
}}>{transI18n("freeSr")}</a></li>
|
||||
<li>
|
||||
<>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/json"
|
||||
id="database-data-upload"
|
||||
className="hidden"
|
||||
onChange={handleImportDatabase}
|
||||
/>
|
||||
<button
|
||||
onClick={() => document.getElementById('database-data-upload')?.click()}
|
||||
>
|
||||
{transI18n("database")}
|
||||
</button>
|
||||
</>
|
||||
</li>
|
||||
<li><a onClick={() => {
|
||||
setImportModal("enka")
|
||||
setIsOpenImport(true)
|
||||
handleShow("import_modal")
|
||||
}}>{transI18n("enka")}</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
<details>
|
||||
<summary className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium">{transI18n("exportData")}</summary>
|
||||
<ul className="p-2">
|
||||
<li><a onClick={() => downloadJson("freesr-data", converterToFreeSRJson(avatars, battle_config))}>{transI18n("freeSr")}</a></li>
|
||||
<li><a onClick={() => downloadJson("database-data", { avatars: avatars, battle_config: battle_config })}>{transI18n("database")}</a></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||
onClick={() => handleShow("connection_modal")}
|
||||
>
|
||||
{transI18n("connectSetting")}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button
|
||||
disabled
|
||||
className="disabled:opacity-50 px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
|
||||
>
|
||||
{transI18n("monsterSetting")} ({transI18n("comingSoon")})
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Right side items */}
|
||||
<div className="navbar-end gap-2">
|
||||
<div className="px-2">
|
||||
<div className="flex items-center space-x-2 p-1.5 rounded-full shadow-md">
|
||||
<div className={`hidden lg:block text-sm italic ${isConnectPS ? 'text-green-500' : 'text-red-500'}`}>
|
||||
{isConnectPS ? transI18n("connected") : transI18n("unconnected")}
|
||||
</div>
|
||||
<div className={`w-3 h-3 rounded-full ${isConnectPS ? 'bg-green-500' : 'bg-red-500'}`}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Language selector - REFINED */}
|
||||
<div className="dropdown dropdown-end">
|
||||
<div className="flex items-center gap-1 border border-base-300 rounded text-sm px-1.5 py-0.5 hover:bg-base-200 cursor-pointer transition-all duration-200">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-4">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="m10.5 21 5.25-11.25L21 21m-9-3h7.5M3 5.621a48.474 48.474 0 0 1 6-.371m0 0c1.12 0 2.233.038 3.334.114M9 5.25V3m3.334 2.364C11.176 10.658 7.69 15.08 3 17.502m9.334-12.138c.896.061 1.785.147 2.666.257m-4.589 8.495a18.023 18.023 0 0 1-3.827-5.802" />
|
||||
</svg>
|
||||
|
||||
<select
|
||||
className="outline-none bg-base-200 cursor-pointer text-sm pr-0"
|
||||
value={locale}
|
||||
onChange={(e) => changeLocale(e.target.value)}
|
||||
>
|
||||
{Object.entries(listCurrentLanguage).map(([key, value]) => (
|
||||
<option key={key} value={key}>{value}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div title="Change Theme" className="dropdown dropdown-end">
|
||||
<div tabIndex={0} role="button" className="btn btn-ghost btn-sm hover:bg-base-200 transition-all duration-200 px-2">
|
||||
<svg
|
||||
width={16}
|
||||
height={16}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
className="h-4 w-4 stroke-current"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
width="10px"
|
||||
height="10px"
|
||||
className="ml-1 h-2 w-2 fill-current opacity-60"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 2048 2048"
|
||||
>
|
||||
<path d="M1799 349l242 241-1017 1017L7 590l242-241 775 775 775-775z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="dropdown-content bg-base-100 text-base-content rounded-lg top-px overflow-y-auto border border-base-200 shadow-md mt-12"
|
||||
>
|
||||
<ul className="menu w-40 p-1">
|
||||
{themes.map((theme) => (
|
||||
<li
|
||||
key={theme.label}
|
||||
onClick={() => {
|
||||
if (changeTheme) changeTheme(theme.label.toLowerCase());
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className="gap-2 px-2 py-1.5 hover:bg-base-200 rounded text-sm transition-all duration-200"
|
||||
data-set-theme={theme.label.toLowerCase()}
|
||||
data-act-class="[&_svg]:visible"
|
||||
>
|
||||
<div
|
||||
data-theme={theme.label.toLowerCase()}
|
||||
className="bg-base-100 grid shrink-0 grid-cols-2 gap-0.5 rounded p-1 shadow-sm"
|
||||
>
|
||||
<div className="bg-base-content size-1 rounded-full" />
|
||||
<div className="bg-primary size-1 rounded-full" />
|
||||
<div className="bg-secondary size-1 rounded-full" />
|
||||
<div className="bg-accent size-1 rounded-full" />
|
||||
</div>
|
||||
<div className="text-sm">{theme.label}</div>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GitHub Link */}
|
||||
<Link
|
||||
className='hidden sm:flex btn btn-ghost btn-sm btn-circle bg-white/20 hover:bg-white/100 transition-all duration-200 items-center justify-center'
|
||||
href={"https://github.com/AzenKain/Firefly-Srtools"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<svg className="w-5 h-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 496 512">
|
||||
<path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3 .3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5 .3-6.2 2.3zm44.2-1.7c-2.9 .7-4.9 2.6-4.6 4.9 .3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3 .7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3 .3 2.9 2.3 3.9 1.6 1 3.6 .7 4.3-.7 .7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3 .7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3 .7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<dialog id="connection_modal" className="modal sm:modal-middle backdrop-blur-sm">
|
||||
<div className="modal-box w-11/12 max-w-7xl 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={() => handleCloseModal("connection_modal")}
|
||||
>
|
||||
✕
|
||||
</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-gradient-to-r from-pink-400 to-cyan-400">
|
||||
{"PS Connection"}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4">
|
||||
{/* Select connection type */}
|
||||
<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) => setConnectionType(e.target.value)}
|
||||
>
|
||||
<option value="FireflyGo">FireflyGo</option>
|
||||
<option value="Other">{transI18n("other")}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Show host/port if Other */}
|
||||
{connectionType === "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="flex flex-col sm:flex-row justify-between items-center mt-6 mb-2">
|
||||
<div className="mb-4 sm:mb-0">
|
||||
<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>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={
|
||||
async () => {
|
||||
const response = await connectToPS()
|
||||
if (response.success) {
|
||||
setIsConnectPS(true)
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("connectedSuccess")
|
||||
})
|
||||
} else {
|
||||
setIsConnectPS(false)
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: response.message
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
className={`btn btn-primary`}
|
||||
|
||||
>
|
||||
{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`}
|
||||
>
|
||||
{transI18n("sync")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
|
||||
<dialog id="import_modal" className="modal backdrop-blur-sm">
|
||||
<div className="modal-box w-11/12 max-w-max 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={() => {
|
||||
handleCloseModal("import_modal")
|
||||
setIsOpenImport(false)
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</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-gradient-to-r from-pink-400 to-cyan-400">
|
||||
{transI18n("importSetting")}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{importModal === "enka" && <EnkaImport />}
|
||||
{importModal === "freesr" && <FreeSRImport />}
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
279
src/components/importBar/copy.tsx
Normal file
279
src/components/importBar/copy.tsx
Normal file
@@ -0,0 +1,279 @@
|
||||
"use client"
|
||||
import useModelStore from "@/stores/modelStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { useEffect, useState } from "react";
|
||||
import useCopyProfileStore from "@/stores/copyProfile";
|
||||
import ProfileCard from "../card/profileCard";
|
||||
import { AvatarProfileCardType, AvatarProfileStore } from "@/types";
|
||||
import Image from "next/image";
|
||||
import SelectCustom from "../select";
|
||||
import useListAvatarStore from "@/stores/avatarStore";
|
||||
import { getNameChar } from "@/helper";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function CopyImport() {
|
||||
const { avatars, setAvatar } = useUserDataStore();
|
||||
const {avatarSelected} = useListAvatarStore()
|
||||
const { locale } = useLocaleStore()
|
||||
const {
|
||||
selectedProfiles,
|
||||
listCopyAvatar,
|
||||
avatarCopySelected,
|
||||
setSelectedProfiles,
|
||||
filterCopy,
|
||||
setFilterCopy,
|
||||
setAvatarCopySelected
|
||||
} = useCopyProfileStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const [listPath, setListPath] = useState<Record<string, boolean>>({ "knight": false, "mage": false, "priest": false, "rogue": false, "shaman": false, "warlock": false, "warrior": false, "memory": false })
|
||||
const [listElement, setListElement] = useState<Record<string, boolean>>({ "fire": false, "ice": false, "imaginary": false, "physical": false, "quantum": false, "thunder": false, "wind": false })
|
||||
const [listRank, setListRank] = useState<Record<string, boolean>>({ "4": false, "5": false })
|
||||
const [message, setMessage] = useState({
|
||||
type: "",
|
||||
text: ""
|
||||
})
|
||||
|
||||
const handleProfileToggle = (profile: AvatarProfileCardType) => {
|
||||
if (selectedProfiles.some((selectedProfile) => selectedProfile.key === profile.key)) {
|
||||
setSelectedProfiles(selectedProfiles.filter((selectedProfile) => selectedProfile.key !== profile.key));
|
||||
return;
|
||||
}
|
||||
setSelectedProfiles([...selectedProfiles, profile]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setFilterCopy({
|
||||
...filterCopy,
|
||||
locale: locale,
|
||||
path: Object.keys(listPath).filter((key) => listPath[key]),
|
||||
element: Object.keys(listElement).filter((key) => listElement[key]),
|
||||
rarity: Object.keys(listRank).filter((key) => listRank[key])
|
||||
})
|
||||
}, [listPath, listRank, locale])
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedProfiles([]);
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("clearAll")
|
||||
})
|
||||
};
|
||||
|
||||
const selectAll = () => {
|
||||
if (avatarCopySelected) {
|
||||
setSelectedProfiles(avatars[avatarCopySelected?.id.toString()].profileList.map((profile, index) => {
|
||||
if (!profile.lightcone?.item_id && Object.keys(profile.relics).length == 0) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
key: index,
|
||||
...profile,
|
||||
} as AvatarProfileCardType
|
||||
}).filter((profile) => profile !== null));
|
||||
}
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("selectedAll")
|
||||
})
|
||||
};
|
||||
|
||||
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",
|
||||
text: transI18n("noAvatarSelected")
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const newListProfile = avatars[avatarCopySelected.id.toString()].profileList.map((profile, index) => {
|
||||
if (!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 newAvatar = {
|
||||
...avatars[avatarSelected.id.toString()],
|
||||
profileList: avatars[avatarSelected.id.toString()].profileList.concat(newListProfile),
|
||||
profileSelect: avatars[avatarSelected.id.toString()].profileList.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-10 mt-2 w-full">
|
||||
{/* Path */}
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2 justify-start items-center">
|
||||
{Object.entries(listPath).map(([key], index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setListPath((prev) => ({ ...prev, [key]: !prev[key] }))
|
||||
}}
|
||||
className="w-[50px] h-[50px] hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<Image
|
||||
src={`https://api.hakush.in/hsr/UI/pathicon/${key}.webp`}
|
||||
alt={key}
|
||||
className="h-[32px] w-[32px] 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(listElement).map(([key], index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setListElement((prev) => ({ ...prev, [key]: !prev[key] }))
|
||||
}}
|
||||
className="w-[50px] h-[50px] hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: listElement[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<Image
|
||||
src={`https://api.hakush.in/hsr/UI/element/${key}.webp`}
|
||||
alt={key}
|
||||
className="h-[28px] w-[28px] 2xl:h-[40px] 2xl:w-[40px] 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((prev) => ({ ...prev, [key]: !prev[key] }))
|
||||
}}
|
||||
className="w-[50px] h-[50px] 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-[32px] w-[32px] text-center flex items-center justify-center">{key}*</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<div>
|
||||
<div>{transI18n("characterName")}</div>
|
||||
{listCopyAvatar.length > 0 && (
|
||||
<SelectCustom
|
||||
customSet={listCopyAvatar.map((avatar) => ({
|
||||
value: avatar.id.toString(),
|
||||
label: getNameChar(locale, avatar),
|
||||
imageUrl: `https://api.hakush.in/hsr/UI/avatarshopicon/${avatar.id}.webp`
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={avatarCopySelected?.id.toString() || ""}
|
||||
placeholder="Character Select"
|
||||
setSelectedCustomSet={(value) => setAvatarCopySelected(listCopyAvatar.find((avatar) => avatar.id.toString() === 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">
|
||||
{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>
|
||||
)
|
||||
}
|
||||
222
src/components/importBar/enka.tsx
Normal file
222
src/components/importBar/enka.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
"use client"
|
||||
import { useState } from "react";
|
||||
import CharacterInfoCard from "../card/characterInfoCard";
|
||||
import useEnkaStore from "@/stores/enkaStore";
|
||||
import { SendDataThroughProxy } from "@/lib/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";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function EnkaImport() {
|
||||
const {
|
||||
uidInput,
|
||||
setUidInput,
|
||||
enkaData,
|
||||
selectedCharacters,
|
||||
setSelectedCharacters,
|
||||
setEnkaData,
|
||||
} = useEnkaStore();
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { avatars, setAvatar } = useUserDataStore();
|
||||
const { isOpenImport, setIsOpenImport } = useModelStore()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [Error, setError] = useState("")
|
||||
|
||||
const handlerFetchData = async () => {
|
||||
if (!uidInput) {
|
||||
setError(transI18n("pleaseEnterUid"))
|
||||
return;
|
||||
}
|
||||
setIsLoading(true)
|
||||
const data : EnkaResponse = await SendDataThroughProxy({data: {serverUrl: "https://enka.network/api/hsr/uid/" + uidInput, method: "GET"}})
|
||||
if (data) {
|
||||
setEnkaData(data)
|
||||
setSelectedCharacters(data.detailInfo.avatarDetailList.map((character) => {
|
||||
return {
|
||||
key: character.avatarId,
|
||||
avatar_id: character.avatarId,
|
||||
rank: character.rank ?? 0,
|
||||
level: character.level,
|
||||
lightcone: {
|
||||
level: character.equipment.level,
|
||||
rank: character.equipment.rank,
|
||||
item_id: character.equipment.tid,
|
||||
},
|
||||
relics: character.relicList.map((relic) => ({
|
||||
level: relic.level,
|
||||
relic_id: relic.tid,
|
||||
relic_set_id: parseInt(relic.tid.toString().slice(1, -1), 10),
|
||||
})),
|
||||
} as CharacterInfoCardType
|
||||
}))
|
||||
setError("")
|
||||
} else {
|
||||
setError(transI18n("failedToFetchEnkaData"))
|
||||
}
|
||||
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 selectAll = () => {
|
||||
if (enkaData) {
|
||||
setSelectedCharacters(enkaData?.detailInfo.avatarDetailList.map((character) => {
|
||||
return {
|
||||
key: character.avatarId,
|
||||
avatar_id: character.avatarId,
|
||||
rank: character.rank ?? 0,
|
||||
level: character.level,
|
||||
lightcone: {
|
||||
level: character.equipment.level,
|
||||
rank: character.equipment.rank,
|
||||
item_id: character.equipment.tid,
|
||||
},
|
||||
relics: character.relicList.map((relic) => ({
|
||||
level: relic.level,
|
||||
relic_id: relic.tid,
|
||||
relic_set_id: parseInt(relic.tid.toString().slice(1, -1), 10),
|
||||
})),
|
||||
} as CharacterInfoCardType
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
if (selectedCharacters.length === 0) {
|
||||
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
|
||||
return;
|
||||
}
|
||||
if (!enkaData) {
|
||||
setError(transI18n("noDataToImport"));
|
||||
return;
|
||||
}
|
||||
setError("");
|
||||
const listAvatars = { ...avatars }
|
||||
const filterData = enkaData.detailInfo.avatarDetailList.filter((character) => 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
|
||||
newAvatar.promotion = character.promotion
|
||||
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()
|
||||
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">
|
||||
{enkaData?.detailInfo.avatarDetailList.map((character, index) => (
|
||||
<CharacterInfoCard
|
||||
key={character.avatarId}
|
||||
character={{
|
||||
key: character.avatarId,
|
||||
avatar_id: character.avatarId,
|
||||
rank: character.rank ?? 0,
|
||||
level: character.level,
|
||||
lightcone: {
|
||||
level: character.equipment.level,
|
||||
rank: character.equipment.rank,
|
||||
item_id: character.equipment.tid,
|
||||
},
|
||||
relics: character.relicList.map((relic) => ({
|
||||
level: relic.level,
|
||||
relic_id: relic.tid,
|
||||
relic_set_id: parseInt(relic.tid.toString().slice(1, -1), 10),
|
||||
})),
|
||||
} as CharacterInfoCardType
|
||||
}
|
||||
selectedCharacters={selectedCharacters}
|
||||
onCharacterToggle={handleCharacterToggle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
235
src/components/importBar/freesr.tsx
Normal file
235
src/components/importBar/freesr.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
"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";
|
||||
|
||||
export default function FreeSRImport() {
|
||||
const { avatars, setAvatar, setBattleConfig } = useUserDataStore();
|
||||
const { isOpenImport, setIsOpenImport } = useModelStore()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [Error, setError] = useState("")
|
||||
const { freeSRData, setFreeSRData, selectedCharacters, setSelectedCharacters } = useFreeSRStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
|
||||
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 (freeSRData) {
|
||||
setSelectedCharacters(Object.entries(freeSRData?.avatars).map(([key, character]) => {
|
||||
const lightcone = freeSRData.lightcones.find((lightcone) => lightcone.equip_avatar === character.avatar_id)
|
||||
const relics = freeSRData.relics.filter((relic) => relic.equip_avatar === character.avatar_id)
|
||||
return {
|
||||
key: character.avatar_id,
|
||||
avatar_id: character.avatar_id,
|
||||
rank: character.data.rank ?? 0,
|
||||
level: character.level,
|
||||
lightcone: {
|
||||
level: lightcone?.level ?? 0,
|
||||
rank: lightcone?.rank ?? 0,
|
||||
item_id: lightcone?.item_id ?? "",
|
||||
},
|
||||
relics: relics.map((relic) => ({
|
||||
level: relic.level,
|
||||
relic_id: relic.relic_id,
|
||||
relic_set_id: relic.relic_set_id,
|
||||
})),
|
||||
} as CharacterInfoCardType
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
reader.onload = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.target?.result as string);
|
||||
const parsed = freeSRJsonSchema.parse(data)
|
||||
setFreeSRData(parsed)
|
||||
setError("")
|
||||
|
||||
setSelectedCharacters(Object.entries(parsed?.avatars || {}).map(([key, character]) => {
|
||||
const lightcone = parsed?.lightcones.find((lightcone) => lightcone.equip_avatar === character.avatar_id)
|
||||
const relics = parsed?.relics.filter((relic) => relic.equip_avatar === character.avatar_id)
|
||||
return {
|
||||
key: character.avatar_id,
|
||||
avatar_id: character.avatar_id,
|
||||
rank: character.data.rank ?? 0,
|
||||
level: character.level,
|
||||
lightcone: {
|
||||
level: lightcone?.level ?? 0,
|
||||
rank: lightcone?.rank ?? 0,
|
||||
item_id: lightcone?.item_id ?? "",
|
||||
},
|
||||
relics: relics?.map((relic) => ({
|
||||
level: relic.level,
|
||||
relic_id: relic.relic_id,
|
||||
relic_set_id: relic.relic_set_id,
|
||||
})) ?? [],
|
||||
} as CharacterInfoCardType
|
||||
}));
|
||||
} catch (error) {
|
||||
setSelectedCharacters([])
|
||||
setFreeSRData(null)
|
||||
setError(transI18n("fileIsNotAValidFreeSRJsonFile"))
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
setIsLoading(false)
|
||||
}
|
||||
setIsLoading(false)
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
if (selectedCharacters.length === 0) {
|
||||
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
|
||||
return;
|
||||
}
|
||||
if (!freeSRData) {
|
||||
setError(transI18n("noDataToImport"));
|
||||
return;
|
||||
}
|
||||
setError("");
|
||||
|
||||
const listAvatars = { ...avatars }
|
||||
const filterData = Object.entries(freeSRData?.avatars || {}).filter(([key, character]) => selectedCharacters.some((selectedCharacter) => selectedCharacter.avatar_id === character.avatar_id))
|
||||
filterData.forEach(([key, 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(freeSRData, newAvatar.profileList.length, character.avatar_id)
|
||||
if (newProfile) {
|
||||
newAvatar.profileList.push(newProfile)
|
||||
newAvatar.profileSelect = newAvatar.profileList.length - 1
|
||||
}
|
||||
setAvatar(newAvatar)
|
||||
}
|
||||
|
||||
})
|
||||
setBattleConfig(freeSRData?.battle_config)
|
||||
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">
|
||||
{Object.entries(freeSRData?.avatars || {}).map(([key, character]) => {
|
||||
const lightcone = freeSRData?.lightcones.find((lightcone) => lightcone.equip_avatar === character.avatar_id)
|
||||
const relics = freeSRData?.relics.filter((relic) => relic.equip_avatar === character.avatar_id)
|
||||
return (
|
||||
<CharacterInfoCard
|
||||
key={character.avatar_id}
|
||||
character={{
|
||||
key: character.avatar_id,
|
||||
avatar_id: character.avatar_id,
|
||||
rank: character.data.rank ?? 0,
|
||||
level: character.level,
|
||||
lightcone: {
|
||||
level: lightcone?.level ?? 0,
|
||||
rank: lightcone?.rank ?? 0,
|
||||
item_id: lightcone?.item_id ?? "",
|
||||
},
|
||||
relics: relics?.map((relic) => ({
|
||||
level: relic.level,
|
||||
relic_id: relic.relic_id,
|
||||
relic_set_id: relic.relic_set_id,
|
||||
})) ?? [],
|
||||
} as CharacterInfoCardType
|
||||
}
|
||||
selectedCharacters={selectedCharacters}
|
||||
onCharacterToggle={handleCharacterToggle}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
113
src/components/lightconeBar/index.tsx
Normal file
113
src/components/lightconeBar/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import Image from "next/image";
|
||||
import useLocaleStore from "@/stores/localeStore"
|
||||
import useLightconeStore from "@/stores/lightconeStore";
|
||||
import LightconeCard from "../card/lightconeCard";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import useAvatarStore from "@/stores/avatarStore";
|
||||
import useModelStore from "@/stores/modelStore";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
export default function LightconeBar() {
|
||||
const [listPath, setListPath] = useState<Record<string, boolean>>({ "knight": false, "mage": false, "priest": false, "rogue": false, "shaman": false, "warlock": false, "warrior": false, "memory": false })
|
||||
const [listRank, setListRank] = useState<Record<string, boolean>>({ "3": false, "4": false, "5": false })
|
||||
const { locale } = useLocaleStore()
|
||||
const { listLightcone, filter, setFilter } = useLightconeStore()
|
||||
const { setAvatar, avatars } = useUserDataStore()
|
||||
const { avatarSelected } = useAvatarStore()
|
||||
const { setIsOpenLightcone } = useModelStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
useEffect(() => {
|
||||
setFilter({
|
||||
...filter,
|
||||
locale: locale,
|
||||
path: Object.keys(listPath).filter((key) => listPath[key]),
|
||||
rarity: Object.keys(listRank).filter((key) => listRank[key])
|
||||
})
|
||||
}, [listPath, listRank, 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-gradient-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={filter.name}
|
||||
onChange={(e) => setFilter({ ...filter, name: e.target.value, locale: locale })}
|
||||
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="grid grid-cols-12 mt-1 w-full">
|
||||
<div className="grid justify-items-start col-span-8">
|
||||
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-8 mb-1 mx-1 gap-2">
|
||||
{Object.entries(listPath).map(([key, value], index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setListPath((prev) => ({ ...prev, [key]: !prev[key] }))
|
||||
}}
|
||||
className="w-[50px] h-[50px] hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-lg cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<Image src={"https://api.hakush.in/hsr/UI/pathicon/" + key + ".webp"}
|
||||
alt={key}
|
||||
className="h-[32px] w-[32px] object-contain rounded-md "
|
||||
width={200}
|
||||
height={200} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid justify-items-end col-span-4 w-full">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 mb-1 mx-1 gap-2">
|
||||
{Object.entries(listRank).map(([key, value], index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setListRank((prev) => ({ ...prev, [key]: !prev[key] }))
|
||||
}}
|
||||
className="w-[50px] h-[50px] hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-lg cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: listRank[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<div className="font-bold text-white h-[32px] w-[32px] text-center flex items-center justify-center">{key}*</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 md:grid-cols-6 lg:grid-cols-8 mt-2 gap-2">
|
||||
{listLightcone.map((item, index) => (
|
||||
<div key={index} onClick={() => {
|
||||
if (avatarSelected) {
|
||||
const avatar = avatars[avatarSelected.id]
|
||||
avatar.profileList[avatar.profileSelect].lightcone = {
|
||||
level: 80,
|
||||
item_id: Number(item.id),
|
||||
rank: 1,
|
||||
promotion: 6
|
||||
}
|
||||
setAvatar({ ...avatar })
|
||||
setIsOpenLightcone(false)
|
||||
}
|
||||
}}>
|
||||
<LightconeCard data={item} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
16
src/components/parseText/index.tsx
Normal file
16
src/components/parseText/index.tsx
Normal file
@@ -0,0 +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>;
|
||||
}
|
||||
439
src/components/relicBar/index.tsx
Normal file
439
src/components/relicBar/index.tsx
Normal file
@@ -0,0 +1,439 @@
|
||||
"use client";
|
||||
import useRelicStore from '@/stores/relicStore';
|
||||
import useUserDataStore from '@/stores/userDataStore';
|
||||
import { AffixDetail, RelicDetail } from '@/types';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import SelectCustom from '../select';
|
||||
import { calcAffixBonus, randomPartition, randomStep, replaceByParam } from '@/helper';
|
||||
import useAffixStore from '@/stores/affixStore';
|
||||
import { mapingStats } from '@/lib/constant';
|
||||
import useAvatarStore from '@/stores/avatarStore';
|
||||
import useModelStore from '@/stores/modelStore';
|
||||
import useRelicMakerStore from '@/stores/relicMakerStore';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function RelicMaker() {
|
||||
const { avatars, setAvatars } = useUserDataStore()
|
||||
const { avatarSelected } = useAvatarStore()
|
||||
const { setIsOpenRelic } = useModelStore()
|
||||
const { mapRelicInfo } = useRelicStore()
|
||||
const { mapMainAffix, mapSubAffix} = useAffixStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const {
|
||||
selectedRelicSlot,
|
||||
selectedRelicSet,
|
||||
selectedMainStat,
|
||||
listSelectedSubStats,
|
||||
selectedRelicLevel,
|
||||
preSelectedSubStats,
|
||||
setSelectedRelicSet,
|
||||
setSelectedMainStat,
|
||||
setSelectedRelicLevel,
|
||||
setListSelectedSubStats,
|
||||
resetHistory,
|
||||
popHistory,
|
||||
addHistory,
|
||||
} = useRelicMakerStore()
|
||||
|
||||
const relicSets = useMemo(() => {
|
||||
const listSet: Record<string, RelicDetail> = {};
|
||||
for (const [key, value] of Object.entries(mapRelicInfo || {})) {
|
||||
let isOk = false;
|
||||
for (const key2 of Object.keys(value.Parts)) {
|
||||
if (key2.endsWith(selectedRelicSlot)) {
|
||||
isOk = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isOk) {
|
||||
listSet[key] = value;
|
||||
}
|
||||
}
|
||||
return listSet;
|
||||
}, [mapRelicInfo, selectedRelicSlot]);
|
||||
|
||||
const subAffixOptions = useMemo(() => {
|
||||
const listSet: Record<string, AffixDetail> = {};
|
||||
const subAffixMap = mapSubAffix["5"];
|
||||
const mainAffixMap = mapMainAffix["5" + selectedRelicSlot]
|
||||
|
||||
if (Object.keys(subAffixMap || {}).length === 0 || Object.keys(mainAffixMap || {}).length === 0) return listSet;
|
||||
|
||||
for (const [key, value] of Object.entries(subAffixMap)) {
|
||||
if (value.property !== mainAffixMap[selectedMainStat]?.property) {
|
||||
listSet[key] = value;
|
||||
}
|
||||
}
|
||||
return listSet;
|
||||
}, [mapSubAffix, mapMainAffix, selectedRelicSlot, selectedMainStat]);
|
||||
|
||||
useEffect(() => {
|
||||
const subAffixMap = mapSubAffix["5"];
|
||||
const mainAffixMap = mapMainAffix["5" + selectedRelicSlot];
|
||||
|
||||
if (!subAffixMap || !mainAffixMap) return;
|
||||
|
||||
const mainProp = mainAffixMap[selectedMainStat]?.property;
|
||||
if (!mainProp) return;
|
||||
|
||||
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
|
||||
let updated = false;
|
||||
|
||||
for (let i = 0; i < newSubAffixes.length; i++) {
|
||||
if (newSubAffixes[i].property === mainProp) {
|
||||
newSubAffixes[i].affixId = "";
|
||||
newSubAffixes[i].property = "";
|
||||
newSubAffixes[i].rollCount = 0;
|
||||
newSubAffixes[i].stepCount = 0;
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) setListSelectedSubStats(newSubAffixes);
|
||||
}, [selectedMainStat, mapSubAffix, mapMainAffix, selectedRelicSlot]);
|
||||
|
||||
const exSubAffixOptions = useMemo(() => {
|
||||
const listSet: Record<string, AffixDetail> = {};
|
||||
const subAffixMap = mapSubAffix["5"];
|
||||
const mainAffixMap = mapMainAffix["5" + selectedRelicSlot];
|
||||
|
||||
if (!subAffixMap || !mainAffixMap) return listSet;
|
||||
|
||||
for (const [key, value] of Object.entries(subAffixMap)) {
|
||||
const subAffix = listSelectedSubStats.find((item) => item.property === value.property);
|
||||
if (subAffix && value.property !== mainAffixMap[selectedMainStat]?.property) {
|
||||
listSet[key] = value;
|
||||
}
|
||||
}
|
||||
return listSet;
|
||||
}, [mapSubAffix, listSelectedSubStats, mapMainAffix, selectedRelicSlot, selectedMainStat]);
|
||||
|
||||
const effectBonus = useMemo(() => {
|
||||
const affixSet = mapMainAffix?.["5" + selectedRelicSlot];
|
||||
if (!affixSet) return 0;
|
||||
|
||||
const data = affixSet[selectedMainStat];
|
||||
if (!data) return 0;
|
||||
|
||||
const stat = mapingStats?.[data.property];
|
||||
if (!stat) return 0;
|
||||
|
||||
const value = data.base + data.step * selectedRelicLevel;
|
||||
|
||||
if (stat.unit === "%") {
|
||||
return (value * 100).toFixed(1) + "%";
|
||||
}
|
||||
|
||||
return value.toFixed(0) + stat.unit;
|
||||
}, [mapMainAffix, selectedRelicSlot, selectedMainStat, selectedRelicLevel]);
|
||||
|
||||
const handleSubStatChange = (key: string, index: number, rollCount: number, stepCount: number) => {
|
||||
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
|
||||
if (!subAffixOptions[key]) {
|
||||
newSubAffixes[index].affixId = "";
|
||||
newSubAffixes[index].property = "";
|
||||
newSubAffixes[index].rollCount = rollCount;
|
||||
newSubAffixes[index].stepCount = stepCount;
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
addHistory(index, newSubAffixes[index]);
|
||||
return;
|
||||
}
|
||||
newSubAffixes[index].affixId = key;
|
||||
newSubAffixes[index].property = subAffixOptions[key].property;
|
||||
newSubAffixes[index].rollCount = rollCount;
|
||||
newSubAffixes[index].stepCount = stepCount;
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
addHistory(index, newSubAffixes[index]);
|
||||
};
|
||||
|
||||
const handlerRollback = (index: number) => {
|
||||
if (!preSelectedSubStats[index]) return;
|
||||
|
||||
const keys = Object.keys(preSelectedSubStats[index]);
|
||||
if (keys.length <= 1) return;
|
||||
|
||||
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
|
||||
const listHistory = preSelectedSubStats[index].map(item => ({ ...item }));
|
||||
const secondLastKey = listHistory.length - 2;
|
||||
const preSubAffixes = { ...listHistory[secondLastKey] };
|
||||
newSubAffixes[index].rollCount = preSubAffixes.rollCount;
|
||||
newSubAffixes[index].stepCount = preSubAffixes.stepCount;
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
popHistory(index);
|
||||
};
|
||||
|
||||
const resetSubStat = (index: number) => {
|
||||
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
|
||||
resetHistory(index);
|
||||
newSubAffixes[index].affixId = "";
|
||||
newSubAffixes[index].property = "";
|
||||
newSubAffixes[index].rollCount = 0;
|
||||
newSubAffixes[index].stepCount = 0;
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
};
|
||||
|
||||
const randomizeStats = () => {
|
||||
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
|
||||
const exKeys = Object.keys(exSubAffixOptions);
|
||||
for (let i = 0; i < newSubAffixes.length; i++) {
|
||||
const keys = Object.keys(subAffixOptions).filter((key) => !exKeys.includes(key));
|
||||
const randomKey = keys[Math.floor(Math.random() * keys.length )];
|
||||
exKeys.push(randomKey);
|
||||
const randomValue = subAffixOptions[randomKey];
|
||||
newSubAffixes[i].affixId = randomKey;
|
||||
newSubAffixes[i].property = randomValue.property;
|
||||
newSubAffixes[i].rollCount = 0;
|
||||
newSubAffixes[i].stepCount = 0;
|
||||
}
|
||||
for (let i = 0; i < newSubAffixes.length; i++) {
|
||||
addHistory(i, newSubAffixes[i]);
|
||||
}
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
|
||||
};
|
||||
|
||||
const randomizeRolls = () => {
|
||||
const newSubAffixes = listSelectedSubStats.map(item => ({ ...item }));
|
||||
const randomRolls = randomPartition(9, listSelectedSubStats.length);
|
||||
for (let i = 0; i < listSelectedSubStats.length; i++) {
|
||||
newSubAffixes[i].rollCount = randomRolls[i];
|
||||
newSubAffixes[i].stepCount = randomStep(randomRolls[i]);
|
||||
}
|
||||
setListSelectedSubStats(newSubAffixes);
|
||||
for (let i = 0; i < newSubAffixes.length; i++) {
|
||||
addHistory(i, newSubAffixes[i]);
|
||||
}
|
||||
};
|
||||
|
||||
const handlerSaveRelic = () => {
|
||||
const avatar = avatars[avatarSelected?.id || ""];
|
||||
if (!selectedRelicSet || !selectedMainStat || !selectedRelicLevel || !selectedRelicSlot) {
|
||||
toast.error(transI18n("pleaseSelectAllOptions"));
|
||||
return;
|
||||
};
|
||||
if (avatar) {
|
||||
avatar.profileList[avatar.profileSelect].relics[selectedRelicSlot] = {
|
||||
level: selectedRelicLevel,
|
||||
relic_id: Number(`6${selectedRelicSet}${selectedRelicSlot}`),
|
||||
relic_set_id: Number(selectedRelicSet),
|
||||
main_affix_id: Number(selectedMainStat),
|
||||
sub_affixes: listSelectedSubStats.map((item) => {
|
||||
return {
|
||||
sub_affix_id: Number(item.affixId),
|
||||
count: item.rollCount,
|
||||
step: item.stepCount
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
setAvatars({ ...avatars });
|
||||
setIsOpenRelic(false);
|
||||
toast.success(transI18n("relicSavedSuccessfully"));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<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-gradient-to-r from-pink-400 to-cyan-400">
|
||||
{transI18n("relicMaker")}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-7xl mx-auto">
|
||||
|
||||
{/* Left Panel */}
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* Set Configuration */}
|
||||
<div className="bg-base-100 rounded-xl p-6 border border-slate-700">
|
||||
<h2 className="text-xl font-bold mb-6 text-warning">{transI18n("mainSettings")}</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
{/* Main Stat */}
|
||||
<div>
|
||||
<label className="block text-lg font-medium mb-2">{transI18n("mainStat")}</label>
|
||||
<SelectCustom
|
||||
customSet={Object.entries(mapMainAffix["5" + selectedRelicSlot] || {}).map(([key, value]) => ({
|
||||
value: key,
|
||||
label: mapingStats[value.property].name + " " + mapingStats[value.property].unit,
|
||||
imageUrl: mapingStats[value.property].icon
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={selectedMainStat}
|
||||
placeholder={transI18n("selectAMainStat")}
|
||||
setSelectedCustomSet={setSelectedMainStat}
|
||||
/>
|
||||
</div>
|
||||
{/* Relic Set Selection */}
|
||||
<div>
|
||||
<label className="block text-lg font-medium mb-2">{transI18n("set")}</label>
|
||||
<SelectCustom
|
||||
customSet={Object.entries(relicSets).map(([key, value]) => ({
|
||||
value: key,
|
||||
label: value.Name,
|
||||
imageUrl: `https://api.hakush.in/hsr/UI/itemfigures/${value.Icon.match(/\d+/)?.[0]}.webp`
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={selectedRelicSet}
|
||||
placeholder={transI18n("selectASet")}
|
||||
setSelectedCustomSet={setSelectedRelicSet}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Set Bonus Display */}
|
||||
<div className="mb-6 py-4 bg-base-100 rounded-lg">
|
||||
{selectedRelicSet !== "" ? Object.entries(mapRelicInfo[selectedRelicSet].RequireNum).map(([key, value]) => (
|
||||
<div key={key} className="text-blue-300 text-sm mb-1">
|
||||
<span className="text-info font-bold">{key}-Pc:
|
||||
<div
|
||||
className="text-warning leading-relaxed font-bold"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: replaceByParam(
|
||||
value.Desc,
|
||||
value.ParamList || []
|
||||
)
|
||||
}}
|
||||
/> </span>
|
||||
</div>
|
||||
)) : <p className="text-blue-300 text-sm font-bold mb-1">{transI18n("pleaseSelectASet")}</p>}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Rarity */}
|
||||
<div className="grid grid-cols-2 items-center gap-4 mb-6">
|
||||
<label className="block text-lg font-medium mb-2">{transI18n("rarity")}: {5} ⭐</label>
|
||||
<label className="block text-lg font-medium mb-2">{transI18n("effectBonus")}: <span className="text-warning font-bold">{effectBonus}</span></label>
|
||||
</div>
|
||||
|
||||
{/* Level */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-lg font-medium mb-2">{transI18n("level")}</label>
|
||||
<div className="bg-base-200 rounded-lg p-4">
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="15"
|
||||
value={selectedRelicLevel}
|
||||
onChange={(e) => setSelectedRelicLevel(parseInt(e.target.value))}
|
||||
className="range range-primary w-full"
|
||||
/>
|
||||
<div className="text-center text-2xl font-bold mt-2">{selectedRelicLevel}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<button onClick={handlerSaveRelic} className="btn btn-success w-full">
|
||||
{transI18n("save")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Panel - Sub Stats */}
|
||||
<div className="space-y-4">
|
||||
{/* Total Roll */}
|
||||
<div className="bg-base-100 rounded-xl p-6 border border-slate-700 z-[1]">
|
||||
<h3 className="text-lg font-bold mb-4">{transI18n("totalRoll")} {listSelectedSubStats.reduce((a, b) => a + b.rollCount, 0)}</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<button
|
||||
className="btn btn-outline btn-success"
|
||||
onClick={randomizeStats}
|
||||
>
|
||||
{transI18n("randomizeStats")}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline btn-success"
|
||||
onClick={randomizeRolls}
|
||||
>
|
||||
{transI18n("randomizeRolls")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{listSelectedSubStats.map((v, index) => (
|
||||
<div key={index} className={`bg-base-100 rounded-xl p-4 border border-slate-700`}>
|
||||
<div className="grid grid-cols-12 gap-2 items-center">
|
||||
|
||||
{/* Stat Selection */}
|
||||
<div className="col-span-8">
|
||||
<SelectCustom
|
||||
customSet={Object.entries(subAffixOptions).map(([key, value]) => ({
|
||||
value: key,
|
||||
label: mapingStats[value.property].name + " " + mapingStats[value.property].unit,
|
||||
imageUrl: mapingStats[value.property].icon
|
||||
}))}
|
||||
excludeSet={Object.entries(exSubAffixOptions).map(([key, value]) => ({
|
||||
value: key,
|
||||
label: mapingStats[value.property].name + " " + mapingStats[value.property].unit,
|
||||
imageUrl: mapingStats[value.property].icon
|
||||
}))}
|
||||
selectedCustomSet={v.affixId}
|
||||
placeholder={transI18n("selectASubStat")}
|
||||
setSelectedCustomSet={(key) => handleSubStatChange(key, index, 0, 0)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Current Value */}
|
||||
<div className="col-span-4 text-center flex items-center justify-center gap-2">
|
||||
<span className="text-2xl font-mono">+{ }</span>
|
||||
<div className="text-xl font-bold text-info">{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount, v.rollCount)}{mapingStats?.[subAffixOptions[v.affixId]?.property]?.unit || ""}</div>
|
||||
</div>
|
||||
|
||||
{/* Roll Values */}
|
||||
<div className="col-span-12 grid grid-cols-3 gap-1">
|
||||
<button
|
||||
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 0)}
|
||||
className="btn btn-sm bg-white text-slate-800 hover:bg-gray-200 border-0"
|
||||
>
|
||||
{calcAffixBonus(subAffixOptions[v.affixId], 0 , v.rollCount + 1)}{mapingStats?.[subAffixOptions[v.affixId]?.property]?.unit || ""}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 1)}
|
||||
className="btn btn-sm bg-white text-slate-800 hover:bg-gray-200 border-0"
|
||||
>
|
||||
{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + 1, v.rollCount + 1)}{mapingStats?.[subAffixOptions[v.affixId]?.property]?.unit || ""}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSubStatChange(v.affixId, index, v.rollCount + 1, v.stepCount + 2)}
|
||||
className="btn btn-sm bg-white text-slate-800 hover:bg-gray-200 border-0"
|
||||
>
|
||||
{calcAffixBonus(subAffixOptions[v.affixId], v.stepCount + 2, v.rollCount + 1)}{mapingStats?.[subAffixOptions[v.affixId]?.property]?.unit || ""}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Reset Button & Roll Info */}
|
||||
<div className="col-span-12 text-center">
|
||||
<div className="grid grid-cols-2 gap-1 items-center justify-items-start">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="btn btn-error btn-sm mb-1"
|
||||
onClick={() => resetSubStat(index)}
|
||||
>
|
||||
{transI18n("reset")}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-warning btn-sm mb-1"
|
||||
onClick={() => handlerRollback(index)}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-lg flex items-center gap-2 text-gray-400">
|
||||
<span className="font-bold">{transI18n("roll")}: <span className="text-info">{v.rollCount}</span></span>
|
||||
<span className="font-bold">{transI18n("step")}: <span className="text-info">{v.stepCount}</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
293
src/components/relicsInfo/index.tsx
Normal file
293
src/components/relicsInfo/index.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
"use client"
|
||||
;
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import RelicMaker from "../relicBar";
|
||||
import { motion } from "framer-motion";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import { useTranslations } from "next-intl";
|
||||
import RelicCard from "../card/relicCard";
|
||||
import useAvatarStore from "@/stores/avatarStore";
|
||||
import useRelicStore from "@/stores/relicStore";
|
||||
import useModelStore from '@/stores/modelStore';
|
||||
import { replaceByParam } from '@/helper';
|
||||
import useRelicMakerStore from '@/stores/relicMakerStore';
|
||||
import useAffixStore from '@/stores/affixStore';
|
||||
|
||||
export default function RelicsInfo() {
|
||||
const router = useRouter()
|
||||
const { avatars, setAvatars } = useUserDataStore()
|
||||
const { avatarSelected } = useAvatarStore()
|
||||
const {
|
||||
setSelectedRelicSlot,
|
||||
setSelectedMainStat,
|
||||
setSelectedRelicSet,
|
||||
setSelectedRelicLevel,
|
||||
setListSelectedSubStats,
|
||||
resetHistory,
|
||||
resetSubStat,
|
||||
listSelectedSubStats,
|
||||
} = useRelicMakerStore()
|
||||
const { mapSubAffix } = useAffixStore()
|
||||
const { isOpenRelic, setIsOpenRelic } = useModelStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { locale } = useLocaleStore();
|
||||
const { mapRelicInfo } = useRelicStore()
|
||||
|
||||
const handleShow = (modalId: string) => {
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
setIsOpenRelic(true);
|
||||
modal.showModal();
|
||||
}
|
||||
};
|
||||
|
||||
// Close modal handler
|
||||
const handleCloseModal = (modalId: string) => {
|
||||
setIsOpenRelic(false);
|
||||
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
|
||||
if (modal) {
|
||||
modal.close();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle ESC key to close modal
|
||||
useEffect(() => {
|
||||
if (!isOpenRelic) {
|
||||
handleCloseModal("action_detail_modal");
|
||||
return;
|
||||
};
|
||||
|
||||
const handleEscKey = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape' && isOpenRelic) {
|
||||
handleCloseModal("action_detail_modal");
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleEscKey);
|
||||
return () => window.removeEventListener('keydown', handleEscKey);
|
||||
}, [isOpenRelic]);
|
||||
|
||||
const getRelic = useCallback((slot: string) => {
|
||||
const avatar = avatars[avatarSelected?.id || ""];
|
||||
if (avatar) {
|
||||
return avatar.profileList[avatar.profileSelect]?.relics[slot] || null;
|
||||
}
|
||||
return null;
|
||||
}, [avatars, avatarSelected]);
|
||||
|
||||
const handlerDeleteRelic = (slot: string) => {
|
||||
const avatar = avatars[avatarSelected?.id || ""];
|
||||
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 = mapSubAffix["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)
|
||||
}
|
||||
|
||||
handleShow("action_detail_modal")
|
||||
}
|
||||
|
||||
const getEffects = useMemo(() => {
|
||||
const avatar = avatars[avatarSelected?.id || ""];
|
||||
const relicCount: { [key: string]: number } = {};
|
||||
if (avatar) {
|
||||
for (const [slot, relic] of Object.entries(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]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<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-gradient-to-b from-primary to-primary/50 rounded-full"></div>
|
||||
Equipment
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-2xl mx-auto">
|
||||
{["1", "2", "3", "4", "5", "6"].map((item, index) => (
|
||||
<div key={index} className="relative group">
|
||||
<div
|
||||
onClick={() => {
|
||||
handlerChangeRelic(item)
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<RelicCard
|
||||
slot={item}
|
||||
avatarId={avatarSelected?.id || ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Action buttons - chỉ hiện khi hover */}
|
||||
<div className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex gap-1">
|
||||
{/* Nút chuyển relic */}
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handlerChangeRelic(item)
|
||||
}}
|
||||
className="btn btn-info p-1.5 rounded-full shadow-lg transition-colors duration-200"
|
||||
title="Chuyển relic"
|
||||
>
|
||||
<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(`Bạn có chắc muốn xóa relic ở slot ${item}?`)) {
|
||||
handlerDeleteRelic(item)
|
||||
}
|
||||
}}
|
||||
className="btn btn-error p-1.5 rounded-full shadow-lg transition-colors duration-200"
|
||||
title="Xóa relic"
|
||||
>
|
||||
<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>
|
||||
</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-gradient-to-b from-primary to-primary/50 rounded-full"></div>
|
||||
Set Effects
|
||||
</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
{getEffects.map((setEffect, index) => {
|
||||
const relicInfo = mapRelicInfo[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(
|
||||
relicInfo.Name,
|
||||
[]
|
||||
)
|
||||
}}
|
||||
/>
|
||||
{setEffect.count && (
|
||||
<span className={`text-sm text-info`}>
|
||||
({setEffect.count})
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 pl-4">
|
||||
{Object.entries(relicInfo.RequireNum).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(
|
||||
value.Desc,
|
||||
value.ParamList || []
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog id="action_detail_modal" className="modal lg:backdrop-blur-sm z-10">
|
||||
<div className="modal-box w-11/12 max-w-7xl 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={() => handleCloseModal("action_detail_modal")}
|
||||
>
|
||||
✕
|
||||
</motion.button>
|
||||
</div>
|
||||
<RelicMaker />
|
||||
</div>
|
||||
|
||||
</dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
67
src/components/select/index.tsx
Normal file
67
src/components/select/index.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
'use client'
|
||||
import Select, { SingleValue } from 'react-select'
|
||||
import Image from 'next/image'
|
||||
import useLocaleStore from '@/stores/localeStore'
|
||||
import ParseText from '../parseText'
|
||||
|
||||
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 SelectCustom({ customSet, excludeSet, selectedCustomSet, placeholder, setSelectedCustomSet }: SelectCustomProp) {
|
||||
const options: SelectOption[] = customSet
|
||||
const { locale } = useLocaleStore()
|
||||
const customStyles = {
|
||||
option: (provided: any) => ({
|
||||
...provided,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px'
|
||||
}),
|
||||
singleValue: (provided: any) => ({
|
||||
...provided,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}),
|
||||
menuPortal: (provided: any) => ({ ...provided, zIndex: 9999 }),
|
||||
menu: (provided: any) => ({ ...provided, zIndex: 9999 })
|
||||
}
|
||||
|
||||
const formatOptionLabel = (option: SelectOption) => (
|
||||
<div className="flex items-center gap-1 bg-slate-400 w-full h-full z-50">
|
||||
<Image src={option.imageUrl} alt="" width={125} height={125} className="w-8 h-8 object-contain" />
|
||||
<ParseText className='text-black 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>) => {
|
||||
console.log(selected);
|
||||
setSelectedCustomSet(selected?.value || '')
|
||||
}}
|
||||
formatOptionLabel={formatOptionLabel}
|
||||
styles={customStyles}
|
||||
placeholder={placeholder}
|
||||
className="my-react-select-container"
|
||||
classNamePrefix="my-react-select"
|
||||
isSearchable
|
||||
isClearable
|
||||
/>
|
||||
)
|
||||
}
|
||||
8
src/components/themeController/clientThemeWrapper.tsx
Normal file
8
src/components/themeController/clientThemeWrapper.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
"use client";
|
||||
import { PropsWithChildren, useContext } from "react";
|
||||
import { ThemeContext } from "./themeContext";
|
||||
|
||||
export function ClientThemeWrapper({ children }: PropsWithChildren) {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
return <div data-theme={theme} className="h-full">{children}</div>;
|
||||
}
|
||||
2
src/components/themeController/index.ts
Normal file
2
src/components/themeController/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./clientThemeWrapper"
|
||||
export * from "./themeContext"
|
||||
41
src/components/themeController/themeContext.tsx
Normal file
41
src/components/themeController/themeContext.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
import { createContext, PropsWithChildren, useEffect, useState } from "react";
|
||||
|
||||
interface ThemeContextType {
|
||||
theme?: string;
|
||||
changeTheme?: (nextTheme: string | null) => void;
|
||||
}
|
||||
export const ThemeContext = createContext<ThemeContextType>({});
|
||||
|
||||
export const ThemeProvider = ({ children }: PropsWithChildren) => {
|
||||
const [theme, setTheme] = useState<string>("light");
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
const storedTheme = localStorage.getItem("theme");
|
||||
if (storedTheme) setTheme(storedTheme);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const changeTheme = (nextTheme: string | null) => {
|
||||
console.log(nextTheme)
|
||||
if (nextTheme) {
|
||||
setTheme(nextTheme);
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("theme", nextTheme);
|
||||
}
|
||||
} else {
|
||||
setTheme((prev) => (prev === "light" ? "night" : "light"));
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("theme", theme);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, changeTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
50
src/helper/calcData.ts
Normal file
50
src/helper/calcData.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { mapingStats } from "@/lib/constant"
|
||||
import { AffixDetail } from "@/types"
|
||||
|
||||
export function calcPromotion(level: number) {
|
||||
if (level < 20) {
|
||||
return 0
|
||||
}
|
||||
if (level < 30) {
|
||||
return 1
|
||||
}
|
||||
if (level < 40) {
|
||||
return 2
|
||||
}
|
||||
if (level < 50) {
|
||||
return 3
|
||||
}
|
||||
if (level < 60) {
|
||||
return 4
|
||||
}
|
||||
if (level < 70) {
|
||||
return 5
|
||||
}
|
||||
return 6
|
||||
}
|
||||
|
||||
|
||||
export function calcRarity(rarity: string) {
|
||||
if (rarity.includes("5")) {
|
||||
return 5
|
||||
}
|
||||
if (rarity.includes("4")) {
|
||||
return 4
|
||||
}
|
||||
if (rarity.includes("3")) {
|
||||
return 3
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
export const calcAffixBonus = (affix: AffixDetail, stepCount: number, rollCount: number) => {
|
||||
const data = affix;
|
||||
if (!data) return 0;
|
||||
if (mapingStats?.[data.property].unit === "%") {
|
||||
return ((data.base * rollCount + data.step * stepCount) * 100).toFixed(1);
|
||||
}
|
||||
if (mapingStats?.[data.property].name === "SPD") {
|
||||
return (data.base * rollCount + data.step * stepCount).toFixed(1);
|
||||
}
|
||||
return (data.base * rollCount + data.step * stepCount).toFixed(0);
|
||||
}
|
||||
87
src/helper/connect.ts
Normal file
87
src/helper/connect.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
"use client"
|
||||
import { SendDataThroughProxy, SendDataToServer } from "@/lib/api"
|
||||
import useConnectStore from "@/stores/connectStore"
|
||||
import useUserDataStore from "@/stores/userDataStore"
|
||||
import { converterToFreeSRJson } from "./converterToFreeSRJson"
|
||||
import { psResponseSchema } from "@/zod"
|
||||
|
||||
export const connectToPS = async (): Promise<{ success: boolean, message: string }> => {
|
||||
const {
|
||||
connectionType,
|
||||
privateType,
|
||||
serverUrl,
|
||||
username,
|
||||
password
|
||||
} = useConnectStore.getState()
|
||||
|
||||
let urlQuery = serverUrl
|
||||
if (!urlQuery.startsWith("http://") && !urlQuery.startsWith("https://")) {
|
||||
urlQuery = `http://${urlQuery}`
|
||||
}
|
||||
if (connectionType === "FireflyGo") {
|
||||
urlQuery = "http://localhost:21000/sync"
|
||||
} else if (connectionType === "Other" && privateType === "Server") {
|
||||
const response = await SendDataThroughProxy({data: {username, password, serverUrl, data: null, method: "POST"}})
|
||||
if (response instanceof Error) {
|
||||
return { success: false, message: response.message }
|
||||
} else if (response.error) {
|
||||
return { success: false, message: response.error }
|
||||
} else {
|
||||
const parsed = psResponseSchema.safeParse(response.data)
|
||||
if (!parsed.success) {
|
||||
return { success: false, message: "Invalid response schema" }
|
||||
}
|
||||
return { success: true, message: "" }
|
||||
}
|
||||
}
|
||||
const response = await SendDataToServer(username, password, urlQuery, null)
|
||||
if (typeof response === "string") {
|
||||
return { success: false, message: response }
|
||||
} else if (response.status != 200) {
|
||||
return { success: false, message: response.message }
|
||||
} else {
|
||||
return { success: true, message: "" }
|
||||
}
|
||||
}
|
||||
|
||||
export const syncDataToPS = async (): Promise<{ success: boolean, message: string }> => {
|
||||
const {
|
||||
connectionType,
|
||||
privateType,
|
||||
serverUrl,
|
||||
username,
|
||||
password
|
||||
} = useConnectStore.getState()
|
||||
|
||||
const {avatars, battle_config} = useUserDataStore.getState()
|
||||
const data = converterToFreeSRJson(avatars, battle_config)
|
||||
|
||||
let urlQuery = serverUrl
|
||||
if (!urlQuery.startsWith("http://") && !urlQuery.startsWith("https://")) {
|
||||
urlQuery = `http://${urlQuery}`
|
||||
}
|
||||
if (connectionType === "FireflyGo") {
|
||||
urlQuery = "http://localhost:21000/sync"
|
||||
} else if (connectionType === "Other" && privateType === "Server") {
|
||||
const response = await SendDataThroughProxy({data: {username, password, serverUrl, data, method: "POST"}})
|
||||
if (response instanceof Error) {
|
||||
return { success: false, message: response.message }
|
||||
} else if (response.error) {
|
||||
return { success: false, message: response.error }
|
||||
} else {
|
||||
const parsed = psResponseSchema.safeParse(response.data)
|
||||
if (!parsed.success) {
|
||||
return { success: false, message: "Invalid response schema" }
|
||||
}
|
||||
return { success: true, message: "" }
|
||||
}
|
||||
}
|
||||
const response = await SendDataToServer(username, password, urlQuery, data)
|
||||
if (typeof response === "string") {
|
||||
return { success: false, message: response }
|
||||
} else if (response.status != 200) {
|
||||
return { success: false, message: response.message }
|
||||
} else {
|
||||
return { success: true, message: "" }
|
||||
}
|
||||
}
|
||||
89
src/helper/convertData.ts
Normal file
89
src/helper/convertData.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { CharacterBasic, CharacterBasicRaw, LightConeBasic, LightConeBasicRaw, RelicBasic, RelicBasicEffect, RelicBasicRaw } from "@/types";
|
||||
|
||||
export function convertRelicSet(id: string, item: RelicBasicRaw): RelicBasic {
|
||||
let lang = new Map<string, string>([
|
||||
['en', item.en],
|
||||
['kr', item.kr],
|
||||
['cn', item.cn],
|
||||
['jp', item.jp]
|
||||
]);
|
||||
|
||||
const setRelic = new Map<string, RelicBasicEffect>();
|
||||
|
||||
Object.entries(item.set).forEach(([key, value]) => {
|
||||
setRelic.set(key, {
|
||||
ParamList: value.ParamList,
|
||||
lang: new Map<string, string>([
|
||||
['en', value.en],
|
||||
['kr', value.kr],
|
||||
['cn', value.cn],
|
||||
['jp', value.jp]
|
||||
])
|
||||
});
|
||||
});
|
||||
|
||||
const result: RelicBasic = {
|
||||
icon: item.icon,
|
||||
lang: lang,
|
||||
id: id,
|
||||
set: setRelic
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function convertLightcone(id: string, item: LightConeBasicRaw): LightConeBasic {
|
||||
|
||||
let lang = new Map<string, string>([
|
||||
['en', item.en],
|
||||
['kr', item.kr],
|
||||
['cn', item.cn],
|
||||
['jp', item.jp]
|
||||
]);
|
||||
const result: LightConeBasic = {
|
||||
rank: item.rank,
|
||||
baseType: item.baseType,
|
||||
desc: item.desc,
|
||||
lang: lang,
|
||||
id: id
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
export function convertAvatar(id: string, item: CharacterBasicRaw): CharacterBasic {
|
||||
|
||||
let lang = new Map<string, string>([
|
||||
['en', item.en],
|
||||
['kr', item.kr],
|
||||
['cn', item.cn],
|
||||
['jp', item.jp]
|
||||
]);
|
||||
let text = ""
|
||||
if (Number(id) % 2 === 0 && Number(id) > 8000) {
|
||||
text = `Female ${item.damageType} MC`
|
||||
} else if (Number(id) > 8000) {
|
||||
text = `Male ${item.damageType} MC`
|
||||
}
|
||||
if (text !== "") {
|
||||
lang.set("en", text)
|
||||
lang.set("kr", text)
|
||||
lang.set("cn", text)
|
||||
lang.set("jp", text)
|
||||
}
|
||||
const result: CharacterBasic = {
|
||||
release: item.release,
|
||||
icon: item.icon,
|
||||
rank: item.rank,
|
||||
baseType: item.baseType,
|
||||
damageType: item.damageType,
|
||||
desc: item.desc,
|
||||
lang: lang,
|
||||
id: id
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
100
src/helper/converterToAvatarStore.ts
Normal file
100
src/helper/converterToAvatarStore.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { AvatarEnkaDetail, AvatarProfileStore, AvatarStore, CharacterDetail, EnkaResponse, FreeSRJson, RelicStore } from "@/types";
|
||||
|
||||
function safeNumber(val: any, fallback = 0): number {
|
||||
const num = Number(val);
|
||||
return Number.isFinite(num) && num !== 0 ? num : fallback;
|
||||
}
|
||||
|
||||
export function converterToAvatarStore(data: Record<string, CharacterDetail>): { [key: string]: AvatarStore } {
|
||||
return Object.fromEntries(
|
||||
Object.entries(data).map(([key, value]) => [
|
||||
key,
|
||||
{
|
||||
owner_uid: 0,
|
||||
avatar_id: Number(key),
|
||||
data: {
|
||||
rank: 0,
|
||||
skills: Object.entries(value.SkillTrees).reduce((acc, [pointName, dataPointEntry]) => {
|
||||
const firstEntry = Object.values(dataPointEntry)[0];
|
||||
if (firstEntry) {
|
||||
acc[firstEntry.PointID] = firstEntry.MaxLevel;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, number>)
|
||||
},
|
||||
level: 80,
|
||||
promotion: 6,
|
||||
techniques: [],
|
||||
sp_max: safeNumber(value.SPNeed, 100),
|
||||
can_change_sp: safeNumber(value.SPNeed) !== 0,
|
||||
sp_value: Math.ceil(safeNumber(value.SPNeed) / 2),
|
||||
profileSelect: 0,
|
||||
enhanced: "",
|
||||
profileList: [{
|
||||
profile_name: "Default",
|
||||
lightcone: null,
|
||||
relics: {} as Record<string, RelicStore>
|
||||
} as AvatarProfileStore]
|
||||
}
|
||||
])
|
||||
) as { [key: string]: AvatarStore }
|
||||
}
|
||||
|
||||
export function converterOneEnkaDataToAvatarStore(data: AvatarEnkaDetail, count: number): AvatarProfileStore | null {
|
||||
if (!data.equipment && (!data.relicList || data.relicList.length === 0)) return null
|
||||
const profile: AvatarProfileStore = {
|
||||
profile_name: `Enka Profile ${count}`,
|
||||
lightcone: {
|
||||
level: data.equipment.level,
|
||||
item_id: data.equipment.tid,
|
||||
rank: data.equipment.rank,
|
||||
promotion: data.equipment.promotion,
|
||||
},
|
||||
relics: Object.fromEntries(data.relicList.map((relic) => [relic.tid.toString()[relic.tid.toString().length - 1], {
|
||||
level: relic.level,
|
||||
relic_id: relic.tid,
|
||||
relic_set_id: parseInt(relic.tid.toString().slice(1, -1), 10),
|
||||
main_affix_id: relic.mainAffixId,
|
||||
sub_affixes: relic.subAffixList.map((subAffix) => ({
|
||||
sub_affix_id: subAffix.affixId,
|
||||
count: subAffix.cnt,
|
||||
step: subAffix.step ?? 0
|
||||
}))
|
||||
}]))
|
||||
}
|
||||
return profile
|
||||
}
|
||||
|
||||
|
||||
export function converterOneFreeSRDataToAvatarStore(data: FreeSRJson, count: number , avatar_id: number): AvatarProfileStore | null {
|
||||
const lightcone = data.lightcones.find((lightcone) => lightcone.equip_avatar === avatar_id)
|
||||
const relics = data.relics.filter((relic) => relic.equip_avatar === avatar_id)
|
||||
if (!lightcone && (!relics || relics.length === 0)) return null
|
||||
const relicsMap = {} as Record<string, RelicStore>
|
||||
|
||||
relics.forEach((relic) => {
|
||||
relicsMap[relic.relic_id.toString()[relic.relic_id.toString().length - 1]] = {
|
||||
level: relic.level,
|
||||
relic_id: relic.relic_id,
|
||||
relic_set_id: relic.relic_set_id,
|
||||
main_affix_id: relic.main_affix_id,
|
||||
sub_affixes: relic.sub_affixes.map((subAffix) => ({
|
||||
sub_affix_id: subAffix.sub_affix_id,
|
||||
count: subAffix.count,
|
||||
step: subAffix.step ?? 0
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
const profile: AvatarProfileStore = {
|
||||
profile_name: `FreeSR Profile ${count}`,
|
||||
lightcone: {
|
||||
level: lightcone?.level ?? 0,
|
||||
item_id: lightcone?.item_id ?? 0,
|
||||
rank: lightcone?.rank ?? 0,
|
||||
promotion: lightcone?.promotion ?? 0,
|
||||
},
|
||||
relics: relicsMap
|
||||
}
|
||||
return profile
|
||||
}
|
||||
71
src/helper/converterToFreeSRJson.ts
Normal file
71
src/helper/converterToFreeSRJson.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { AvatarJson, AvatarStore, BattleConfigJson, BattleConfigStore, FreeSRJson, LightconeJson, RelicJson } from "@/types";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
|
||||
export function converterToFreeSRJson(avatars: Record<string, AvatarStore>, battle_config: BattleConfigStore): FreeSRJson {
|
||||
const lightcones: LightconeJson[] = []
|
||||
const relics: RelicJson[] = []
|
||||
const battleJson: BattleConfigJson = {
|
||||
battle_type: battle_config.battle_type,
|
||||
blessings: battle_config.blessings,
|
||||
custom_stats: battle_config.custom_stats,
|
||||
cycle_count: battle_config.cycle_count,
|
||||
stage_id: battle_config.stage_id,
|
||||
path_resonance_id: battle_config.path_resonance_id,
|
||||
monsters: battle_config.monsters,
|
||||
}
|
||||
const avatarsJson: { [key: string]: AvatarJson } = {}
|
||||
let internalUidLightcone = 0
|
||||
let internalUidRelic = 0
|
||||
Object.entries(avatars).forEach(([avatarId, avatar]) => {
|
||||
avatarsJson[avatarId] = {
|
||||
owner_uid: avatar.owner_uid,
|
||||
avatar_id: avatar.avatar_id,
|
||||
data: avatar.data,
|
||||
level: avatar.level,
|
||||
promotion: avatar.promotion,
|
||||
techniques: avatar.techniques,
|
||||
sp_value: avatar.sp_value,
|
||||
sp_max: avatar.sp_max,
|
||||
}
|
||||
const currentProfile = avatar.profileList[avatar.profileSelect]
|
||||
if (currentProfile.lightcone && currentProfile.lightcone.item_id !== 0) {
|
||||
const newLightcone: LightconeJson = {
|
||||
level: currentProfile.lightcone.level,
|
||||
item_id: currentProfile.lightcone.item_id,
|
||||
rank: currentProfile.lightcone.rank,
|
||||
promotion: currentProfile.lightcone.promotion,
|
||||
internal_uid: internalUidLightcone,
|
||||
equip_avatar: avatar.avatar_id,
|
||||
}
|
||||
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: relic.level,
|
||||
relic_id: relic.relic_id,
|
||||
relic_set_id: relic.relic_set_id,
|
||||
main_affix_id: relic.main_affix_id,
|
||||
sub_affixes: relic.sub_affixes,
|
||||
internal_uid: internalUidRelic,
|
||||
equip_avatar: avatar.avatar_id,
|
||||
}
|
||||
internalUidRelic++
|
||||
relics.push(newRelic)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
return {
|
||||
lightcones,
|
||||
relics,
|
||||
avatars: avatarsJson,
|
||||
battle_config: battleJson,
|
||||
}
|
||||
}
|
||||
15
src/helper/getAvatarNotExist.ts
Normal file
15
src/helper/getAvatarNotExist.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import useAvatarStore from "@/stores/avatarStore";
|
||||
import { CharacterDetail } from "@/types";
|
||||
|
||||
export function getAvatarNotExist(): Record<string, CharacterDetail> {
|
||||
const { avatars } = useUserDataStore.getState()
|
||||
const { mapAvatarInfo } = useAvatarStore.getState()
|
||||
const listAvatarId = Object.keys(avatars)
|
||||
const listAvatarNotExist = Object.keys(mapAvatarInfo).filter((avatarId) => !listAvatarId.includes(avatarId))
|
||||
return listAvatarNotExist.reduce((acc, avatarId) => {
|
||||
acc[avatarId] = mapAvatarInfo[avatarId]
|
||||
return acc
|
||||
}, {} as Record<string, CharacterDetail>)
|
||||
}
|
||||
45
src/helper/getName.ts
Normal file
45
src/helper/getName.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { listCurrentLanguage } from "@/lib/constant";
|
||||
import { CharacterBasic, LightConeBasic } from "@/types";
|
||||
|
||||
|
||||
export function getNameChar(locale: string, data: CharacterBasic | undefined): string {
|
||||
if (!data) {
|
||||
return ""
|
||||
}
|
||||
if (!listCurrentLanguage.hasOwnProperty(locale)) {
|
||||
return ""
|
||||
}
|
||||
|
||||
let text = data.lang.get(listCurrentLanguage[locale as keyof typeof listCurrentLanguage].toLowerCase()) ?? "";
|
||||
if (!text) {
|
||||
text = data.lang.get("en") ?? "";
|
||||
}
|
||||
if (Number(data.id) % 2 === 0 && Number(data.id) > 8000) {
|
||||
text = `Female ${data.damageType} MC`
|
||||
} else if (Number(data.id) > 8000) {
|
||||
text = `Male ${data.damageType} MC`
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
export function getNameLightcone(locale: string, data: LightConeBasic | undefined): string {
|
||||
if (!data) {
|
||||
return ""
|
||||
}
|
||||
if (!listCurrentLanguage.hasOwnProperty(locale)) {
|
||||
return ""
|
||||
}
|
||||
|
||||
let text = data.lang.get(listCurrentLanguage[locale as keyof typeof listCurrentLanguage].toLowerCase()) ?? "";
|
||||
if (!text) {
|
||||
text = data.lang.get("en") ?? "";
|
||||
}
|
||||
return 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>`;
|
||||
});
|
||||
}
|
||||
23
src/helper/getSkillTree.ts
Normal file
23
src/helper/getSkillTree.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import useAvatarStore from "@/stores/avatarStore"
|
||||
import useUserDataStore from "@/stores/userDataStore"
|
||||
|
||||
export function getSkillTree(enhanced: string) {
|
||||
const { avatarSelected, mapAvatarInfo } = useAvatarStore.getState()
|
||||
|
||||
if (!avatarSelected) return null;
|
||||
if (enhanced != "") return Object.entries(mapAvatarInfo[avatarSelected.id || ""]?.Enhanced[enhanced].SkillTrees || {}).reduce((acc, [pointName, dataPointEntry]) => {
|
||||
const firstEntry = Object.values(dataPointEntry)[0];
|
||||
if (firstEntry) {
|
||||
acc[firstEntry.PointID] = firstEntry.MaxLevel;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, number>)
|
||||
|
||||
return Object.entries(mapAvatarInfo[avatarSelected.id || ""]?.SkillTrees).reduce((acc, [pointName, dataPointEntry]) => {
|
||||
const firstEntry = Object.values(dataPointEntry)[0];
|
||||
if (firstEntry) {
|
||||
acc[firstEntry.PointID] = firstEntry.MaxLevel;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
}
|
||||
9
src/helper/index.ts
Normal file
9
src/helper/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export * from "./getName"
|
||||
export * from "./replaceByParam"
|
||||
export * from "./converterToAvatarStore"
|
||||
export * from "./getAvatarNotExist"
|
||||
export * from "./calcData"
|
||||
export * from "./random"
|
||||
export * from "./json"
|
||||
export * from "./convertData"
|
||||
export * from "./connect"
|
||||
14
src/helper/json.ts
Normal file
14
src/helper/json.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export function downloadJson(fileName: string, data: any) {
|
||||
const json = JSON.stringify(data, null, 2)
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `${fileName}.json`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
27
src/helper/random.ts
Normal file
27
src/helper/random.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export function randomPartition(sum: number, parts: number): number[] {
|
||||
let raw = Array.from({ length: parts }, () => Math.random());
|
||||
let total = raw.reduce((a, b) => a + b, 0);
|
||||
let 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;
|
||||
}
|
||||
26
src/helper/replaceByParam.ts
Normal file
26
src/helper/replaceByParam.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export function replaceByParam(desc: string, params: number[]): string {
|
||||
desc = desc.replace(/<color=#[0-9a-fA-F]{8}>(.*?)<\/color>/g, (match, inner) => {
|
||||
const colorCode = match.match(/#[0-9a-fA-F]{8}/)?.[0] ?? "#ffffff";
|
||||
const processed = inner.replace(/#(\d+)\[i\](%)?/g, (_: string, index: string, percent: string | undefined) => {
|
||||
const i = parseInt(index, 10) - 1;
|
||||
const value = params[i];
|
||||
if (value === undefined) return "";
|
||||
return percent ? `${(value * 100).toFixed(0)}%` : `${value}`;
|
||||
}).replace(/<unbreak>(.*?)<\/unbreak>/g, "$1");
|
||||
|
||||
return `<span style="color:${colorCode}">${processed}</span>`;
|
||||
});
|
||||
|
||||
desc = desc.replace(/<unbreak>#(\d+)\[i\](%)?<\/unbreak>/g, (_: string, index: string, percent: string | undefined) => {
|
||||
const i = parseInt(index, 10) - 1;
|
||||
const value = params[i];
|
||||
if (value === undefined) return "";
|
||||
return percent ? `${(value * 100).toFixed(0)}%` : `${value}`;
|
||||
});
|
||||
|
||||
desc = desc.replace(/<unbreak>(\d+)<\/unbreak>/g, (_, number) => number);
|
||||
|
||||
desc = desc.replaceAll("\\n", "<br></br>");
|
||||
|
||||
return desc;
|
||||
}
|
||||
4
src/hooks/useChangeTheme.ts
Normal file
4
src/hooks/useChangeTheme.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { useContext } from "react";
|
||||
import { ThemeContext } from "@/components/themeController";
|
||||
|
||||
export const useChangeTheme = () => useContext(ThemeContext);
|
||||
32
src/lib/affixLoader.ts
Normal file
32
src/lib/affixLoader.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { AffixDetail } from '@/types';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'data');
|
||||
let mainAffixCache: Record<string, Record<string, AffixDetail>> = {};
|
||||
let subAffixCache: Record<string, Record<string, AffixDetail>> = {};
|
||||
|
||||
const mainAffixPath = path.join(DATA_DIR, `main_affixes.json`)
|
||||
const subAffixPath = path.join(DATA_DIR, `sub_affixes.json`)
|
||||
|
||||
function loadFromFileIfExists(filePath: string): Record<string, Record<string, AffixDetail>> {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, Record<string, AffixDetail>>;
|
||||
return data;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
export async function loadMainAffix(): Promise<Record<string, Record<string, AffixDetail>>> {
|
||||
if (Object.keys(mainAffixCache).length > 0) return mainAffixCache;
|
||||
const fileData = loadFromFileIfExists(mainAffixPath);
|
||||
mainAffixCache = fileData;
|
||||
return fileData;
|
||||
}
|
||||
|
||||
export async function loadSubAffix(): Promise<Record<string, Record<string, AffixDetail>>> {
|
||||
if (Object.keys(subAffixCache).length > 0) return subAffixCache;
|
||||
const fileData = loadFromFileIfExists(subAffixPath);
|
||||
subAffixCache = fileData;
|
||||
return fileData;
|
||||
}
|
||||
297
src/lib/api.ts
Normal file
297
src/lib/api.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
|
||||
import { CharacterBasic, CharacterBasicRaw } from "@/types/characterBasic";
|
||||
import { AffixDetail, CharacterDetail, ConfigMaze, EnkaResponse, FreeSRJson, LightConeBasic, LightConeBasicRaw, LightConeDetail, PSResponse, RelicBasic, RelicBasicEffect, RelicBasicRaw, RelicDetail } from "@/types";
|
||||
import axios from 'axios';
|
||||
import { convertAvatar, convertLightcone, convertRelicSet } from "@/helper";
|
||||
import { psResponseSchema } from "@/zod";
|
||||
|
||||
export async function getConfigMazeApi(): Promise<ConfigMaze> {
|
||||
try {
|
||||
const res = await axios.get<ConfigMaze>(
|
||||
`/api/config-maze`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return res.data as ConfigMaze;
|
||||
} catch (error: unknown) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.log(`Error: ${error.response?.status} - ${error.message}`);
|
||||
} else {
|
||||
console.log(`Unexpected error: ${String(error)}`);
|
||||
}
|
||||
return {
|
||||
Avatar: {},
|
||||
MOC: {},
|
||||
AS: {},
|
||||
PF: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getMainAffixApi(): Promise<Record<string, Record<string, AffixDetail>>> {
|
||||
try {
|
||||
const res = await axios.get<Record<string, Record<string, AffixDetail>>>(
|
||||
`/api/main-affixes`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return res.data as Record<string, Record<string, AffixDetail>>;
|
||||
} catch (error: unknown) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.log(`Error: ${error.response?.status} - ${error.message}`);
|
||||
} else {
|
||||
console.log(`Unexpected error: ${String(error)}`);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSubAffixApi(): Promise<Record<string, Record<string, AffixDetail>>> {
|
||||
try {
|
||||
const res = await axios.get<Record<string, Record<string, AffixDetail>>>(
|
||||
`/api/sub-affixes`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return res.data as Record<string, Record<string, AffixDetail>>;
|
||||
} catch (error: unknown) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.log(`Error: ${error.response?.status} - ${error.message}`);
|
||||
} else {
|
||||
console.log(`Unexpected error: ${String(error)}`);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCharacterInfoApi(avatarId: number, locale: string): Promise<CharacterDetail | null> {
|
||||
try {
|
||||
const res = await axios.get<CharacterDetail>(
|
||||
`https://api.hakush.in/hsr/data/${locale}/character/${avatarId}.json`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return res.data as CharacterDetail;
|
||||
} catch (error: unknown) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.log(`Error: ${error.response?.status} - ${error.message}`);
|
||||
} else {
|
||||
console.log(`Unexpected error: ${String(error)}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLightconeInfoApi(lightconeId: number, locale: string): Promise<LightConeDetail | null> {
|
||||
try {
|
||||
const res = await axios.get<LightConeDetail>(
|
||||
`https://api.hakush.in/hsr/data/${locale}/lightcone/${lightconeId}.json`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return res.data as LightConeDetail;
|
||||
} catch (error: unknown) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.log(`Error: ${error.response?.status} - ${error.message}`);
|
||||
} else {
|
||||
console.log(`Unexpected error: ${String(error)}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRelicInfoApi(relicId: number, locale: string): Promise<RelicDetail | null> {
|
||||
try {
|
||||
const res = await axios.get<RelicDetail>(
|
||||
`https://api.hakush.in/hsr/data/${locale}/relicset/${relicId}.json`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return res.data as RelicDetail;
|
||||
} catch (error: unknown) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.log(`Error: ${error.response?.status} - ${error.message}`);
|
||||
} else {
|
||||
console.log(`Unexpected error: ${String(error)}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCharacterByIdNative(id: string, locale: string): Promise<CharacterDetail | null> {
|
||||
try {
|
||||
const res = await axios.get<CharacterDetail>(`/api/${locale}/characters/${id}`);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch character:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchCharactersByIdsNative(ids: string[], locale: string): Promise<Record<string, CharacterDetail>> {
|
||||
try {
|
||||
const res = await axios.post<Record<string, CharacterDetail>>(`/api/${locale}/characters`, { charIds: ids });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch characters:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchLightconeByIdNative(id: string, locale: string): Promise<LightConeDetail | null> {
|
||||
try {
|
||||
const res = await axios.get<LightConeDetail>(`/api/${locale}/lightcones/${id}`);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch lightcone:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchLightconesByIdsNative(ids: string[], locale: string): Promise<Record<string, LightConeDetail>> {
|
||||
try {
|
||||
const res = await axios.post<Record<string, LightConeDetail>>(`/api/${locale}/lightcones`, { lightconeIds: ids });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch lightcones:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchRelicByIdNative(id: string, locale: string): Promise<RelicDetail | null> {
|
||||
try {
|
||||
const res = await axios.get<RelicDetail>(`/api/${locale}/relics/${id}`);
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch relic:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchRelicsByIdsNative(ids: string[], locale: string): Promise<Record<string, RelicDetail>> {
|
||||
try {
|
||||
const res = await axios.post<Record<string, RelicDetail>>(`/api/${locale}/relics`, { relicIds: ids });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch relics:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCharacterListApi(): Promise<CharacterBasic[]> {
|
||||
try {
|
||||
const res = await axios.get<Record<string, CharacterBasicRaw>>(
|
||||
'https://api.hakush.in/hsr/data/character.json',
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const data = new Map(Object.entries(res.data));
|
||||
|
||||
return Array.from(data.entries()).map(([id, it]) => convertAvatar(id, it));
|
||||
} catch (error: unknown) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.log(`Error: ${error.response?.status} - ${error.message}`);
|
||||
} else {
|
||||
console.log(`Unexpected error: ${String(error)}`);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLightconeListApi(): Promise<LightConeBasic[]> {
|
||||
try {
|
||||
const res = await axios.get<Record<string, LightConeBasicRaw>>(
|
||||
'https://api.hakush.in/hsr/data/lightcone.json',
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const data = new Map(Object.entries(res.data));
|
||||
|
||||
return Array.from(data.entries()).map(([id, it]) => convertLightcone(id, it));
|
||||
} catch (error: unknown) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.log(`Error: ${error.response?.status} - ${error.message}`);
|
||||
} else {
|
||||
console.log(`Unexpected error: ${String(error)}`);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getRelicSetListApi(): Promise<RelicBasic[]> {
|
||||
try {
|
||||
const res = await axios.get<Record<string, RelicBasicRaw>>(
|
||||
'https://api.hakush.in/hsr/data/relicset.json',
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const data = new Map(Object.entries(res.data));
|
||||
|
||||
return Array.from(data.entries()).map(([id, it]) => convertRelicSet(id, it));
|
||||
} catch (error: unknown) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
console.log(`Error: ${error.response?.status} - ${error.message}`);
|
||||
} else {
|
||||
console.log(`Unexpected error: ${String(error)}`);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function SendDataToServer(username: string, password: string, serverUrl: string, data: FreeSRJson | null): Promise<PSResponse | string> {
|
||||
try {
|
||||
const response = await axios.post(`${serverUrl}`, { username, password, data })
|
||||
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;
|
||||
}
|
||||
}
|
||||
51
src/lib/characterLoader.ts
Normal file
51
src/lib/characterLoader.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getCharacterInfoApi } from './api';
|
||||
import { CharacterDetail } from '@/types/characterDetail';
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'data');
|
||||
const characterFileCache: Record<string, Record<string, CharacterDetail>> = {};
|
||||
export let characterMap: Record<string, CharacterDetail> = {};
|
||||
|
||||
function getJsonFilePath(locale: string): string {
|
||||
return path.join(DATA_DIR, `characters.${locale}.json`);
|
||||
}
|
||||
|
||||
function loadFromFileIfExists(locale: string): Record<string, CharacterDetail> | null {
|
||||
if (characterFileCache[locale]) return characterFileCache[locale];
|
||||
|
||||
const filePath = getJsonFilePath(locale);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, CharacterDetail>;
|
||||
characterFileCache[locale] = data;
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function loadCharacters(charIds: string[], locale: string): Promise<Record<string, CharacterDetail>> {
|
||||
const fileData = loadFromFileIfExists(locale);
|
||||
const fileIds = fileData ? Object.keys(fileData) : [];
|
||||
|
||||
if (fileData && charIds.every(id => fileIds.includes(id))) {
|
||||
characterMap = fileData;
|
||||
return characterMap;
|
||||
}
|
||||
|
||||
const result: Record<string, CharacterDetail> = {};
|
||||
|
||||
await Promise.all(
|
||||
charIds.map(async id => {
|
||||
const info = await getCharacterInfoApi(Number(id), locale);
|
||||
if (info) result[id] = info;
|
||||
})
|
||||
);
|
||||
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
const filePath = getJsonFilePath(locale);
|
||||
fs.writeFileSync(filePath, JSON.stringify(result, null, 2), 'utf-8');
|
||||
|
||||
characterFileCache[locale] = result;
|
||||
characterMap = result;
|
||||
return result;
|
||||
}
|
||||
23
src/lib/configMazeLoader.ts
Normal file
23
src/lib/configMazeLoader.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ConfigMaze } from "@/types";
|
||||
import fs from 'fs';
|
||||
import path from "path";
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'data');
|
||||
|
||||
export let configMazeMap: Record<string, ConfigMaze> = {};
|
||||
|
||||
function loadFromFileIfExists(): Record<string, ConfigMaze> | null {
|
||||
const filePath = path.join(DATA_DIR, 'config_maze.json');
|
||||
if (fs.existsSync(filePath)) {
|
||||
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, ConfigMaze>;
|
||||
configMazeMap = data;
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function loadConfigMaze(): Promise<Record<string, ConfigMaze>> {
|
||||
if (Object.keys(configMazeMap).length > 0) return configMazeMap;
|
||||
const fileData = loadFromFileIfExists();
|
||||
return fileData || {};
|
||||
}
|
||||
128
src/lib/constant.ts
Normal file
128
src/lib/constant.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
export const listCurrentLanguage = {
|
||||
ja: "JP",
|
||||
ko: "KR",
|
||||
en: "US",
|
||||
vi: "VN",
|
||||
zh: "CN"
|
||||
};
|
||||
|
||||
export const listCurrentLanguageApi : Record<string, string> = {
|
||||
ja: "jp",
|
||||
ko: "kr",
|
||||
en: "en",
|
||||
vi: "en",
|
||||
zh: "cn"
|
||||
};
|
||||
|
||||
export const mapingStats = <Record<string, {name: string, icon: string, unit: string}> > {
|
||||
"HPDelta": {
|
||||
name:"HP",
|
||||
icon:"/icon/hp.webp",
|
||||
unit: ""
|
||||
},
|
||||
"AttackDelta": {
|
||||
name:"ATK",
|
||||
icon:"/icon/attack.webp",
|
||||
unit: ""
|
||||
},
|
||||
"HPAddedRatio": {
|
||||
name:"HP",
|
||||
icon:"/icon/hp.webp",
|
||||
unit: "%"
|
||||
},
|
||||
"AttackAddedRatio": {
|
||||
name:"ATK",
|
||||
icon:"/icon/attack.webp",
|
||||
unit: "%"
|
||||
},
|
||||
"DefenceDelta": {
|
||||
name:"DEF",
|
||||
icon:"/icon/defence.webp",
|
||||
unit: ""
|
||||
},
|
||||
"DefenceAddedRatio": {
|
||||
name:"DEF",
|
||||
icon:"/icon/defence.webp",
|
||||
unit: "%"
|
||||
},
|
||||
"SPDDelta": {
|
||||
name:"SPD",
|
||||
icon:"/icon/spd.webp",
|
||||
unit: ""
|
||||
},
|
||||
"CriticalChanceBase": {
|
||||
name:"CRIT Rate",
|
||||
icon:"/icon/crit-rate.webp",
|
||||
unit: "%"
|
||||
},
|
||||
"CriticalDamageBase": {
|
||||
name:"CRIT DMG",
|
||||
icon:"/icon/crit-damage.webp",
|
||||
unit: "%"
|
||||
},
|
||||
"HealRatioBase": {
|
||||
name:"Outgoing Healing Boost",
|
||||
icon:"/icon/healing-boost.webp",
|
||||
unit: "%"
|
||||
},
|
||||
"StatusProbabilityBase": {
|
||||
name:"Effect Hit Rate",
|
||||
icon:"/icon/effect-hit-rate.webp",
|
||||
unit: "%"
|
||||
},
|
||||
"StatusResistanceBase": {
|
||||
name:"Effect RES",
|
||||
icon:"/icon/effect-res.webp",
|
||||
unit: "%"
|
||||
},
|
||||
"BreakDamageAddedRatioBase": {
|
||||
name:"Break Effect",
|
||||
icon:"/icon/break-effect.webp",
|
||||
unit: "%"
|
||||
},
|
||||
"SpeedDelta": {
|
||||
name:"SPD",
|
||||
icon:"/icon/speed.webp",
|
||||
unit: ""
|
||||
},
|
||||
"PhysicalAddedRatio": {
|
||||
name:"Physical DMG Boost",
|
||||
icon:"/icon/physical.webp",
|
||||
unit: "%"
|
||||
},
|
||||
"FireAddedRatio": {
|
||||
name:"Fire DMG Boost",
|
||||
icon:"/icon/fire.webp",
|
||||
unit: "%"
|
||||
},
|
||||
"IceAddedRatio": {
|
||||
name:"Ice DMG Boost",
|
||||
icon:"/icon/ice.webp",
|
||||
unit: "%"
|
||||
},
|
||||
"ThunderAddedRatio": {
|
||||
name:"Thunder DMG Boost",
|
||||
icon:"/icon/thunder.webp",
|
||||
unit: "%"
|
||||
},
|
||||
"WindAddedRatio": {
|
||||
name:"Wind DMG Boost",
|
||||
icon:"/icon/wind.webp",
|
||||
unit: "%"
|
||||
},
|
||||
"QuantumAddedRatio": {
|
||||
name:"Quantum DMG Boost",
|
||||
icon:"/icon/quantum.webp",
|
||||
unit: "%"
|
||||
},
|
||||
"ImaginaryAddedRatio": {
|
||||
name:"Imaginary DMG Boost",
|
||||
icon:"/icon/imaginary.webp",
|
||||
unit: "%"
|
||||
},
|
||||
"SPRatioBase": {
|
||||
name:"Energy Regeneration Rate",
|
||||
icon:"/icon/energy-rate.webp",
|
||||
unit: "%"
|
||||
}
|
||||
}
|
||||
51
src/lib/lighconeLoader.ts
Normal file
51
src/lib/lighconeLoader.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getLightconeInfoApi } from './api';
|
||||
import { LightConeDetail } from '@/types';
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'data');
|
||||
const lightconeFileCache: Record<string, Record<string, LightConeDetail>> = {};
|
||||
export let lightconeMap: Record<string, LightConeDetail> = {};
|
||||
|
||||
function getJsonFilePath(locale: string): string {
|
||||
return path.join(DATA_DIR, `lightcones.${locale}.json`);
|
||||
}
|
||||
|
||||
function loadLightconeFromFileIfExists(locale: string): Record<string, LightConeDetail> | null {
|
||||
if (lightconeFileCache[locale]) return lightconeFileCache[locale];
|
||||
|
||||
const filePath = getJsonFilePath(locale);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, LightConeDetail>;
|
||||
lightconeFileCache[locale] = data;
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function loadLightcones(charIds: string[], locale: string): Promise<Record<string, LightConeDetail>> {
|
||||
const fileData = loadLightconeFromFileIfExists(locale);
|
||||
const fileIds = fileData ? Object.keys(fileData) : [];
|
||||
|
||||
if (fileData && charIds.every(id => fileIds.includes(id))) {
|
||||
lightconeMap = fileData;
|
||||
return lightconeMap;
|
||||
}
|
||||
|
||||
const result: Record<string, LightConeDetail> = {};
|
||||
|
||||
await Promise.all(
|
||||
charIds.map(async id => {
|
||||
const info = await getLightconeInfoApi(Number(id), locale);
|
||||
if (info) result[id] = info;
|
||||
})
|
||||
);
|
||||
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
const filePath = getJsonFilePath(locale);
|
||||
fs.writeFileSync(filePath, JSON.stringify(result, null, 2), 'utf-8');
|
||||
|
||||
lightconeFileCache[locale] = result;
|
||||
lightconeMap = result;
|
||||
return result;
|
||||
}
|
||||
51
src/lib/relicLoader.ts
Normal file
51
src/lib/relicLoader.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { getRelicInfoApi } from './api';
|
||||
import { RelicDetail } from '@/types';
|
||||
|
||||
const DATA_DIR = path.join(process.cwd(), 'data');
|
||||
const relicFileCache: Record<string, Record<string, RelicDetail>> = {};
|
||||
export let relicMap: Record<string, RelicDetail> = {};
|
||||
|
||||
function getJsonFilePath(locale: string): string {
|
||||
return path.join(DATA_DIR, `relics.${locale}.json`);
|
||||
}
|
||||
|
||||
function loadRelicFromFileIfExists(locale: string): Record<string, RelicDetail> | null {
|
||||
if (relicFileCache[locale]) return relicFileCache[locale];
|
||||
|
||||
const filePath = getJsonFilePath(locale);
|
||||
if (fs.existsSync(filePath)) {
|
||||
const data = JSON.parse(fs.readFileSync(filePath, 'utf-8')) as Record<string, RelicDetail>;
|
||||
relicFileCache[locale] = data;
|
||||
return data;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function loadRelics(charIds: string[], locale: string): Promise<Record<string, RelicDetail>> {
|
||||
const fileData = loadRelicFromFileIfExists(locale);
|
||||
const fileIds = fileData ? Object.keys(fileData) : [];
|
||||
|
||||
if (fileData && charIds.every(id => fileIds.includes(id))) {
|
||||
relicMap = fileData;
|
||||
return relicMap;
|
||||
}
|
||||
|
||||
const result: Record<string, RelicDetail> = {};
|
||||
|
||||
await Promise.all(
|
||||
charIds.map(async id => {
|
||||
const info = await getRelicInfoApi(Number(id), locale);
|
||||
if (info) result[id] = info;
|
||||
})
|
||||
);
|
||||
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
const filePath = getJsonFilePath(locale);
|
||||
fs.writeFileSync(filePath, JSON.stringify(result, null, 2), 'utf-8');
|
||||
|
||||
relicFileCache[locale] = result;
|
||||
relicMap = result;
|
||||
return result;
|
||||
}
|
||||
18
src/stores/affixStore.ts
Normal file
18
src/stores/affixStore.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { AffixDetail } from '@/types';
|
||||
import { create } from 'zustand'
|
||||
|
||||
interface AffixState {
|
||||
mapMainAffix: Record<string, Record<string, AffixDetail>>;
|
||||
mapSubAffix: Record<string, Record<string, AffixDetail>>;
|
||||
setMapMainAffix: (newMainAffix: Record<string, Record<string, AffixDetail>>) => void;
|
||||
setMapSubAffix: (newSubAffix: Record<string, Record<string, AffixDetail>>) => void;
|
||||
}
|
||||
|
||||
const useAffixStore = create<AffixState>((set, get) => ({
|
||||
mapMainAffix: {},
|
||||
mapSubAffix: {},
|
||||
setMapMainAffix: (newMainAffix: Record<string, Record<string, AffixDetail>>) => set({ mapMainAffix: newMainAffix }),
|
||||
setMapSubAffix: (newSubAffix: Record<string, Record<string, AffixDetail>>) => set({ mapSubAffix: newSubAffix }),
|
||||
}));
|
||||
|
||||
export default useAffixStore;
|
||||
64
src/stores/avatarStore.ts
Normal file
64
src/stores/avatarStore.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { CharacterBasic, CharacterDetail, FilterAvatarType } from '@/types';
|
||||
import { create } from 'zustand'
|
||||
|
||||
|
||||
interface AvatarState {
|
||||
listAvatar: CharacterBasic[];
|
||||
listRawAvatar: CharacterBasic[];
|
||||
filter: FilterAvatarType;
|
||||
avatarSelected: CharacterBasic | null;
|
||||
mapAvatarInfo: Record<string, CharacterDetail>;
|
||||
setListAvatar: (newListAvatar: CharacterBasic[]) => void;
|
||||
setAvatarSelected: (newAvatarSelected: CharacterBasic) => void;
|
||||
setFilter: (newFilter: FilterAvatarType) => void;
|
||||
setMapAvatarInfo: (avatarId: string, newCharacter: CharacterDetail) => void;
|
||||
setAllMapAvatarInfo: (newCharacter: Record<string, CharacterDetail>) => void;
|
||||
}
|
||||
|
||||
const useAvatarStore = create<AvatarState>((set, get) => ({
|
||||
listAvatar: [],
|
||||
listRawAvatar: [],
|
||||
filter: {
|
||||
name: "",
|
||||
path: [],
|
||||
element: [],
|
||||
rarity: [],
|
||||
locale: "",
|
||||
},
|
||||
avatarSelected: null,
|
||||
mapAvatarInfo: {},
|
||||
setListAvatar: (newListAvatar: CharacterBasic[]) => set({ listAvatar: newListAvatar, listRawAvatar: newListAvatar }),
|
||||
setAvatarSelected: (newAvatarSelected: CharacterBasic) => set({ avatarSelected: newAvatarSelected }),
|
||||
setFilter: (newFilter: FilterAvatarType) => {
|
||||
set({ filter: newFilter })
|
||||
if (newFilter.locale === "") {
|
||||
return
|
||||
}
|
||||
let filteredList = get().listRawAvatar;
|
||||
if (newFilter.name) {
|
||||
filteredList = filteredList.filter((avatar) => {
|
||||
return avatar.lang?.get(newFilter.locale)?.toLowerCase().includes(newFilter.name.toLowerCase()) ?? false;
|
||||
});
|
||||
}
|
||||
if (newFilter.path.length > 0) {
|
||||
filteredList = filteredList.filter((avatar) => {
|
||||
return newFilter.path.some((path) => avatar.baseType?.toLowerCase().includes(path.toLowerCase())) ?? false;
|
||||
});
|
||||
}
|
||||
if (newFilter.element.length > 0) {
|
||||
filteredList = filteredList.filter((avatar) => {
|
||||
return newFilter.element.some((element) => avatar.damageType?.toLowerCase().includes(element.toLowerCase())) ?? false;
|
||||
});
|
||||
}
|
||||
if (newFilter.rarity.length > 0) {
|
||||
filteredList = filteredList.filter((avatar) => {
|
||||
return newFilter.rarity.some((rarity) => avatar.rank?.toLowerCase().includes(rarity.toLowerCase())) ?? false;
|
||||
});
|
||||
}
|
||||
set({ listAvatar: filteredList });
|
||||
},
|
||||
setMapAvatarInfo: (avatarId: string, newCharacter: CharacterDetail) => set((state) => ({ mapAvatarInfo: { ...state.mapAvatarInfo, [avatarId]: newCharacter } })),
|
||||
setAllMapAvatarInfo: (newCharacter: Record<string, CharacterDetail>) => set((state) => ({ mapAvatarInfo: { ...state.mapAvatarInfo, ...newCharacter } })),
|
||||
}));
|
||||
|
||||
export default useAvatarStore;
|
||||
39
src/stores/connectStore.ts
Normal file
39
src/stores/connectStore.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { create } from 'zustand'
|
||||
import { createJSONStorage, persist } from 'zustand/middleware';
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const useConnectStore = create<ConnectState>()(
|
||||
persist(
|
||||
(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 }),
|
||||
}),
|
||||
{
|
||||
name: 'connect-storage',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default useConnectStore;
|
||||
61
src/stores/copyProfile.ts
Normal file
61
src/stores/copyProfile.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { AvatarDataStore, AvatarProfileCardType, CharacterBasic, CharacterInfoCardType, EnkaResponse, FilterAvatarType } from '@/types';
|
||||
import { create } from 'zustand'
|
||||
|
||||
interface CopyProfileState {
|
||||
selectedProfiles: AvatarProfileCardType[];
|
||||
listCopyAvatar: CharacterBasic[];
|
||||
listRawCopyAvatar: CharacterBasic[];
|
||||
filterCopy: FilterAvatarType;
|
||||
avatarCopySelected: CharacterBasic | null;
|
||||
setSelectedProfiles: (newListAvatar: AvatarProfileCardType[]) => void;
|
||||
setAvatarCopySelected: (newAvatarSelected: CharacterBasic | null) => void;
|
||||
setFilterCopy: (newFilter: FilterAvatarType) => void;
|
||||
setListCopyAvatar: (newListAvatar: CharacterBasic[]) => void;
|
||||
}
|
||||
|
||||
const useCopyProfileStore = create<CopyProfileState>((set, get) => ({
|
||||
selectedProfiles: [],
|
||||
listCopyAvatar: [],
|
||||
listRawCopyAvatar: [],
|
||||
filterCopy: {
|
||||
name: "",
|
||||
path: [],
|
||||
element: [],
|
||||
rarity: [],
|
||||
locale: "",
|
||||
},
|
||||
avatarCopySelected: null,
|
||||
setListCopyAvatar: (newListAvatar: CharacterBasic[]) => set({ listCopyAvatar: newListAvatar, listRawCopyAvatar: newListAvatar }),
|
||||
setAvatarCopySelected: (newAvatarSelected: CharacterBasic | null) => set({ avatarCopySelected: newAvatarSelected }),
|
||||
setFilterCopy: (newFilter: FilterAvatarType) => {
|
||||
set({ filterCopy: newFilter })
|
||||
if (newFilter.locale === "") {
|
||||
return
|
||||
}
|
||||
let filteredList = get().listRawCopyAvatar;
|
||||
if (newFilter.name) {
|
||||
filteredList = filteredList.filter((avatar) => {
|
||||
return avatar.lang?.get(newFilter.locale)?.toLowerCase().includes(newFilter.name.toLowerCase()) ?? false;
|
||||
});
|
||||
}
|
||||
if (newFilter.path.length > 0) {
|
||||
filteredList = filteredList.filter((avatar) => {
|
||||
return newFilter.path.some((path) => avatar.baseType?.toLowerCase().includes(path.toLowerCase())) ?? false;
|
||||
});
|
||||
}
|
||||
if (newFilter.element.length > 0) {
|
||||
filteredList = filteredList.filter((avatar) => {
|
||||
return newFilter.element.some((element) => avatar.damageType?.toLowerCase().includes(element.toLowerCase())) ?? false;
|
||||
});
|
||||
}
|
||||
if (newFilter.rarity.length > 0) {
|
||||
filteredList = filteredList.filter((avatar) => {
|
||||
return newFilter.rarity.some((rarity) => avatar.rank?.toLowerCase().includes(rarity.toLowerCase())) ?? false;
|
||||
});
|
||||
}
|
||||
set({ listCopyAvatar: filteredList });
|
||||
},
|
||||
setSelectedProfiles: (newListAvatar: AvatarProfileCardType[]) => set({ selectedProfiles: newListAvatar }),
|
||||
}));
|
||||
|
||||
export default useCopyProfileStore;
|
||||
22
src/stores/enkaStore.ts
Normal file
22
src/stores/enkaStore.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
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) => void;
|
||||
setUidInput: (newUidInput: string) => void;
|
||||
}
|
||||
|
||||
const useEnkaStore = create<EnkaState>((set, get) => ({
|
||||
selectedCharacters: [],
|
||||
enkaData: null,
|
||||
uidInput: "",
|
||||
setUidInput: (newUidInput: string) => set({ uidInput: newUidInput }),
|
||||
setSelectedCharacters: (newListAvatar: CharacterInfoCardType[]) => set({ selectedCharacters: newListAvatar }),
|
||||
setEnkaData: (newEnkaData: EnkaResponse) => set({ enkaData: newEnkaData }),
|
||||
}));
|
||||
|
||||
export default useEnkaStore;
|
||||
18
src/stores/freesrStore.ts
Normal file
18
src/stores/freesrStore.ts
Normal file
@@ -0,0 +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, get) => ({
|
||||
selectedCharacters: [],
|
||||
freeSRData: null,
|
||||
setSelectedCharacters: (newListAvatar: CharacterInfoCardType[]) => set({ selectedCharacters: newListAvatar }),
|
||||
setFreeSRData: (newFreeSRData: FreeSRJson | null) => set({ freeSRData: newFreeSRData }),
|
||||
}));
|
||||
|
||||
export default useFreeSRStore;
|
||||
13
src/stores/globalStore.ts
Normal file
13
src/stores/globalStore.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
interface GlobalState {
|
||||
isConnectPS: boolean;
|
||||
setIsConnectPS: (newIsConnectPS: boolean) => void;
|
||||
}
|
||||
|
||||
const useGlobalStore = create<GlobalState>((set, get) => ({
|
||||
isConnectPS: false,
|
||||
setIsConnectPS: (newIsConnectPS: boolean) => set({ isConnectPS: newIsConnectPS }),
|
||||
}));
|
||||
|
||||
export default useGlobalStore;
|
||||
54
src/stores/lightconeStore.ts
Normal file
54
src/stores/lightconeStore.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { LightConeBasic, FilterLightconeType, LightConeDetail } from '@/types';
|
||||
import { create } from 'zustand'
|
||||
|
||||
interface LightconeState {
|
||||
listLightcone: LightConeBasic[];
|
||||
listRawLightcone: LightConeBasic[];
|
||||
filter: FilterLightconeType;
|
||||
mapLightconeInfo: Record<string, LightConeDetail>;
|
||||
setListLightcone: (newListLightcone: LightConeBasic[]) => void;
|
||||
setFilter: (newFilter: FilterLightconeType) => void;
|
||||
setMapLightconeInfo: (lightconeId: string, newLightcone: LightConeDetail) => void;
|
||||
setAllMapLightconeInfo: (newLightcone: Record<string, LightConeDetail>) => void;
|
||||
}
|
||||
|
||||
const useLightconeStore = create<LightconeState>((set, get) => ({
|
||||
listLightcone: [],
|
||||
listRawLightcone: [],
|
||||
mapLightconeInfo: {},
|
||||
filter: {
|
||||
name: "",
|
||||
path: [],
|
||||
locale: "",
|
||||
rarity: [],
|
||||
},
|
||||
setListLightcone: (newListLightcone: LightConeBasic[]) => set({ listLightcone: newListLightcone, listRawLightcone: newListLightcone }),
|
||||
setFilter: (newFilter: FilterLightconeType) => {
|
||||
set({ filter: newFilter })
|
||||
|
||||
if (newFilter.locale === "") {
|
||||
return
|
||||
}
|
||||
let filteredList = get().listRawLightcone;
|
||||
if (newFilter.name && newFilter.locale) {
|
||||
filteredList = filteredList.filter((lightcone) => {
|
||||
return lightcone.lang?.get(newFilter.locale)?.toLowerCase().includes(newFilter.name.toLowerCase()) ?? false;
|
||||
});
|
||||
}
|
||||
if (newFilter.path && newFilter.path.length > 0) {
|
||||
filteredList = filteredList.filter((lightcone) => {
|
||||
return newFilter.path.some((path) => lightcone.baseType?.toLowerCase().includes(path.toLowerCase())) ?? false;
|
||||
});
|
||||
}
|
||||
if (newFilter.rarity && newFilter.rarity.length > 0) {
|
||||
filteredList = filteredList.filter((lightcone) => {
|
||||
return newFilter.rarity.some((rarity) => lightcone.rank?.toLowerCase().includes(rarity.toLowerCase())) ?? false;
|
||||
});
|
||||
}
|
||||
set({ listLightcone: filteredList });
|
||||
},
|
||||
setMapLightconeInfo: (lightconeId: string, newLightcone: LightConeDetail) => set((state) => ({ mapLightconeInfo: { ...state.mapLightconeInfo, [lightconeId]: newLightcone } })),
|
||||
setAllMapLightconeInfo: (newLightcone: Record<string, LightConeDetail>) => set({ mapLightconeInfo: newLightcone }),
|
||||
}));
|
||||
|
||||
export default useLightconeStore;
|
||||
23
src/stores/localeStore.ts
Normal file
23
src/stores/localeStore.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { create } from 'zustand'
|
||||
import { createJSONStorage, persist } from 'zustand/middleware';
|
||||
|
||||
|
||||
interface LocaleState {
|
||||
locale: string;
|
||||
setLocale: (newLocale: string) => void;
|
||||
}
|
||||
|
||||
const useLocaleStore = create<LocaleState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
locale: "en",
|
||||
setLocale: (newLocale: string) => set({ locale: newLocale }),
|
||||
}),
|
||||
{
|
||||
name: 'locale-storage',
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default useLocaleStore;
|
||||
28
src/stores/mazeStore.ts
Normal file
28
src/stores/mazeStore.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { AffixDetail, ASConfigMaze, AvatarConfigMaze, ConfigMaze, MOCConfigMaze, PFConfigMaze } from '@/types';
|
||||
import { create } from 'zustand'
|
||||
|
||||
interface MazeState {
|
||||
Technique: Record<string, AvatarConfigMaze>;
|
||||
MOC: Record<string, MOCConfigMaze>;
|
||||
AS: Record<string, ASConfigMaze>;
|
||||
PF: Record<string, PFConfigMaze>;
|
||||
setTechnique: (newTechnique: Record<string, AvatarConfigMaze>) => void;
|
||||
setMOC: (newMOC: Record<string, MOCConfigMaze>) => void;
|
||||
setAS: (newAS: Record<string, ASConfigMaze>) => void;
|
||||
setPF: (newPF: Record<string, PFConfigMaze>) => void;
|
||||
setAllData: (newData: ConfigMaze) => void;
|
||||
}
|
||||
|
||||
const useMazeStore = create<MazeState>((set) => ({
|
||||
Technique: {},
|
||||
MOC: {},
|
||||
AS: {},
|
||||
PF: {},
|
||||
setTechnique: (newTechnique: Record<string, AvatarConfigMaze>) => set({ Technique: newTechnique }),
|
||||
setMOC: (newMOC: Record<string, MOCConfigMaze>) => set({ MOC: newMOC }),
|
||||
setAS: (newAS: Record<string, ASConfigMaze>) => set({ AS: newAS }),
|
||||
setPF: (newPF: Record<string, PFConfigMaze>) => set({ PF: newPF }),
|
||||
setAllData: (newData: ConfigMaze) => set({ Technique: newData.Avatar, MOC: newData.MOC, AS: newData.AS, PF: newData.PF }),
|
||||
}));
|
||||
|
||||
export default useMazeStore;
|
||||
34
src/stores/modelStore.ts
Normal file
34
src/stores/modelStore.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { create } from 'zustand'
|
||||
|
||||
|
||||
interface ModelState {
|
||||
isOpenLightcone: boolean;
|
||||
isOpenRelic: boolean;
|
||||
isOpenAvatar: boolean;
|
||||
isOpenCreateProfile: boolean;
|
||||
isOpenImport: boolean;
|
||||
isOpenCopy: boolean;
|
||||
setIsOpenLightcone: (newIsOpenLightcone: boolean) => void;
|
||||
setIsOpenRelic: (newIsOpenRelic: boolean) => void;
|
||||
setIsOpenAvatar: (newIsOpenAvatar: boolean) => void;
|
||||
setIsOpenCreateProfile: (newIsOpenCreateProfile: boolean) => void;
|
||||
setIsOpenImport: (newIsOpenImport: boolean) => void;
|
||||
setIsOpenCopy: (newIsOpenCopy: boolean) => void;
|
||||
}
|
||||
|
||||
const useModelStore = create<ModelState>((set, get) => ({
|
||||
isOpenLightcone: false,
|
||||
isOpenRelic: false,
|
||||
isOpenAvatar: false,
|
||||
isOpenCreateProfile: false,
|
||||
isOpenImport: false,
|
||||
isOpenCopy: false,
|
||||
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 }),
|
||||
}));
|
||||
|
||||
export default useModelStore;
|
||||
214
src/stores/relicMakerStore.ts
Normal file
214
src/stores/relicMakerStore.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
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, get) => ({
|
||||
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 = { ...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 = { ...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 = { ...affix };
|
||||
copied.push(newAffix);
|
||||
newPreSelectedSubStats[index] = copied;
|
||||
return { preSelectedSubStats: newPreSelectedSubStats };
|
||||
}
|
||||
return {}
|
||||
}),
|
||||
}));
|
||||
|
||||
export default useRelicMakerStore;
|
||||
44
src/stores/relicStore.ts
Normal file
44
src/stores/relicStore.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { FilterRelicType, RelicDetail, RelicBasic } from '@/types';
|
||||
import { create } from 'zustand'
|
||||
|
||||
interface RelicState {
|
||||
listRelic: RelicBasic[];
|
||||
listRawRelic: RelicBasic[];
|
||||
filter: FilterRelicType;
|
||||
mapRelicInfo: Record<string, RelicDetail>;
|
||||
setListRelic: (newListRelic: RelicBasic[]) => void;
|
||||
setFilter: (newFilter: FilterRelicType) => void;
|
||||
setMapRelicInfo: (lightconeId: string, newRelic: RelicDetail) => void;
|
||||
setAllMapRelicInfo: (newRelic: Record<string, RelicDetail>) => void;
|
||||
}
|
||||
|
||||
const useRelicStore = create<RelicState>((set, get) => ({
|
||||
listRelic: [],
|
||||
listRawRelic: [],
|
||||
mapRelicInfo: {},
|
||||
filter: {
|
||||
name: "",
|
||||
type: [],
|
||||
locale: "",
|
||||
rarity: [],
|
||||
},
|
||||
setListRelic: (newListRelic: RelicBasic[]) => set({ listRelic: newListRelic, listRawRelic: newListRelic }),
|
||||
setFilter: (newFilter: FilterRelicType) => {
|
||||
set({ filter: newFilter })
|
||||
|
||||
if (newFilter.locale === "") {
|
||||
return
|
||||
}
|
||||
let filteredList = get().listRawRelic;
|
||||
if (newFilter.name && newFilter.locale) {
|
||||
filteredList = filteredList.filter((relic) => {
|
||||
return relic.lang?.get(newFilter.locale)?.toLowerCase().includes(newFilter.name.toLowerCase()) ?? false;
|
||||
});
|
||||
}
|
||||
set({ listRelic: filteredList });
|
||||
},
|
||||
setMapRelicInfo: (lightconeId: string, newRelic: RelicDetail) => set((state) => ({ mapRelicInfo: { ...state.mapRelicInfo, [lightconeId]: newRelic } })),
|
||||
setAllMapRelicInfo: (newRelic: Record<string, RelicDetail>) => set({ mapRelicInfo: newRelic }),
|
||||
}));
|
||||
|
||||
export default useRelicStore;
|
||||
37
src/stores/userDataStore.ts
Normal file
37
src/stores/userDataStore.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { AvatarStore, BattleConfigStore } from '@/types';
|
||||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
|
||||
interface UserDataState {
|
||||
avatars: { [key: string]: AvatarStore };
|
||||
battle_config: BattleConfigStore;
|
||||
setAvatars: (newAvatars: { [key: string]: AvatarStore }) => void;
|
||||
setAvatar: (newAvatar: AvatarStore) => void;
|
||||
setBattleConfig: (newBattleConfig: BattleConfigStore) => void;
|
||||
}
|
||||
|
||||
const useUserDataStore = create<UserDataState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
avatars: {},
|
||||
battle_config: {
|
||||
battle_type: "",
|
||||
blessings: [],
|
||||
custom_stats: [],
|
||||
cycle_count: 0,
|
||||
stage_id: 0,
|
||||
path_resonance_id: 0,
|
||||
monsters: [],
|
||||
},
|
||||
setAvatars: (newAvatars: { [key: string]: AvatarStore }) => set({ avatars: newAvatars }),
|
||||
setAvatar: (newAvatar: AvatarStore) => set((state) => ({ avatars: { ...state.avatars, [newAvatar.avatar_id.toString()]: newAvatar } })),
|
||||
setBattleConfig: (newBattleConfig: BattleConfigStore) => set({ battle_config: newBattleConfig }),
|
||||
}),
|
||||
{
|
||||
name: 'user-data-storage',
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export default useUserDataStore;
|
||||
6
src/types/affix.ts
Normal file
6
src/types/affix.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export type AffixDetail = {
|
||||
property: string;
|
||||
base: number;
|
||||
step: number;
|
||||
step_num: number;
|
||||
}
|
||||
25
src/types/card.ts
Normal file
25
src/types/card.ts
Normal file
@@ -0,0 +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>
|
||||
}
|
||||
23
src/types/characterBasic.ts
Normal file
23
src/types/characterBasic.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export interface CharacterBasicRaw {
|
||||
release: number;
|
||||
icon: string;
|
||||
rank: string;
|
||||
baseType: string;
|
||||
damageType: string;
|
||||
en: string;
|
||||
desc: string;
|
||||
kr: string;
|
||||
cn: string;
|
||||
jp: string;
|
||||
}
|
||||
|
||||
export interface CharacterBasic {
|
||||
id: string;
|
||||
release?: number;
|
||||
icon: string;
|
||||
rank: string;
|
||||
baseType: string;
|
||||
damageType: string;
|
||||
desc: string;
|
||||
lang: Map<string, string>;
|
||||
}
|
||||
160
src/types/characterDetail.ts
Normal file
160
src/types/characterDetail.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
export interface CharacterDetail {
|
||||
Name: string;
|
||||
Desc: string;
|
||||
CharaInfo: CharacterInfo;
|
||||
Rarity: string;
|
||||
AvatarVOTag: string;
|
||||
SPNeed: number | null;
|
||||
BaseType: string;
|
||||
DamageType: string;
|
||||
Ranks: Record<string, RankType>;
|
||||
Skills: Record<string, SkillType>;
|
||||
SkillTrees: Record<string, Record<string, SkillTreePoint>>;
|
||||
Memosprite: Memosprite;
|
||||
Unique: Record<string, UniqueAbility>;
|
||||
Stats: Record<string, Stat>;
|
||||
Relics: Relics;
|
||||
Enhanced: Record<string, EnhancedType>;
|
||||
}
|
||||
|
||||
export interface EnhancedType {
|
||||
Descs: string[];
|
||||
ChangeRankList: any;
|
||||
ChangeSkillTreeList: any;
|
||||
Ranks: Record<string, RankType>;
|
||||
Skills: Record<string, SkillType>;
|
||||
SkillTrees: Record<string, Record<string, SkillTreePoint>>;
|
||||
}
|
||||
|
||||
export interface CharacterInfo {
|
||||
Camp: string | null;
|
||||
VA: VoiceActors;
|
||||
Stories: Record<string, string | null>;
|
||||
Voicelines: string[];
|
||||
}
|
||||
|
||||
export interface VoiceActors {
|
||||
Chinese: string | null;
|
||||
Japanese: string | null;
|
||||
Korean: string | null;
|
||||
English: string | null;
|
||||
}
|
||||
|
||||
export interface RankType {
|
||||
Id: number;
|
||||
Name: string;
|
||||
Desc: string;
|
||||
ParamList: number[];
|
||||
}
|
||||
|
||||
export interface SkillType {
|
||||
Id: number;
|
||||
Name: string;
|
||||
Desc: string;
|
||||
Type: string;
|
||||
Tag: string;
|
||||
SPBase: number | null;
|
||||
BPNeed: number;
|
||||
BPAdd: number;
|
||||
ShowStanceList: number[];
|
||||
SkillComboValueDelta: number | null;
|
||||
Level: Record<string, LevelParams>;
|
||||
}
|
||||
|
||||
export interface LevelParams {
|
||||
Level: number;
|
||||
ParamList: number[];
|
||||
}
|
||||
|
||||
export interface SkillTreePoint {
|
||||
Anchor: string;
|
||||
AvatarPromotionLimit: number | null;
|
||||
AvatarLevelLimit: number | null;
|
||||
DefaultUnlock: boolean;
|
||||
Icon: string;
|
||||
LevelUpSkillID: number[];
|
||||
MaterialList: ItemConfigRow[];
|
||||
MaxLevel: number;
|
||||
ParamList: any[];
|
||||
PointID: number;
|
||||
PointName: string | null;
|
||||
PointDesc: string | null;
|
||||
PointTriggerKey: number;
|
||||
PointType: number;
|
||||
PrePoint: string[];
|
||||
StatusAddList: any[];
|
||||
}
|
||||
|
||||
export interface ItemConfigRow {
|
||||
$type: string;
|
||||
ItemID: number;
|
||||
ItemNum: number;
|
||||
Rarity: string;
|
||||
}
|
||||
|
||||
export interface Memosprite {
|
||||
Name: string;
|
||||
Icon: string;
|
||||
HPBase: string;
|
||||
HPInherit: string;
|
||||
HPSkill: number | null;
|
||||
SpeedBase: string;
|
||||
SpeedInherit: string;
|
||||
SpeedSkill: number;
|
||||
Aggro: number;
|
||||
Skills: Record<string, SpriteSkill>;
|
||||
Talent: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface SpriteSkill {
|
||||
Name: string;
|
||||
Desc: string | null;
|
||||
Type: string | null;
|
||||
Tag: string;
|
||||
SPBase: number | null;
|
||||
BPNeed: number;
|
||||
BPAdd: number | null;
|
||||
ShowStanceList: number[];
|
||||
SkillComboValueDelta: number | null;
|
||||
Level: Record<string, LevelParams>;
|
||||
}
|
||||
|
||||
export interface UniqueAbility {
|
||||
Tag: string;
|
||||
Name: string;
|
||||
Desc: string;
|
||||
Param: number[];
|
||||
}
|
||||
|
||||
export interface Stat {
|
||||
AttackBase: number;
|
||||
AttackAdd: number;
|
||||
DefenceBase: number;
|
||||
DefenceAdd: number;
|
||||
HPBase: number;
|
||||
HPAdd: number;
|
||||
SpeedBase: number;
|
||||
CriticalChance: number;
|
||||
CriticalDamage: number;
|
||||
BaseAggro: number;
|
||||
Cost: ItemConfigRow[];
|
||||
}
|
||||
|
||||
export interface Relics {
|
||||
AvatarID: number;
|
||||
Set4IDList: number[];
|
||||
Set2IDList: number[];
|
||||
PropertyList3: string[];
|
||||
PropertyList4: string[];
|
||||
PropertyList5: string[];
|
||||
PropertyList6: string[];
|
||||
PropertyList: RelicRecommendProperty[];
|
||||
SubAffixPropertyList: string[];
|
||||
ScoreRankList: number[];
|
||||
}
|
||||
|
||||
export interface RelicRecommendProperty {
|
||||
$type: string;
|
||||
RelicType: string;
|
||||
PropertyType: string;
|
||||
}
|
||||
26
src/types/config_maze.ts
Normal file
26
src/types/config_maze.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
|
||||
export type ASConfigMaze = {
|
||||
buff_1: number[];
|
||||
buff_2: number[];
|
||||
maze_buff: number;
|
||||
}
|
||||
|
||||
export type PFConfigMaze = {
|
||||
buff: number[];
|
||||
maze_buff: number;
|
||||
}
|
||||
|
||||
export type MOCConfigMaze = {
|
||||
maze_buff: number;
|
||||
}
|
||||
|
||||
export type AvatarConfigMaze = {
|
||||
maze_buff: number[];
|
||||
}
|
||||
|
||||
export type ConfigMaze = {
|
||||
Avatar: Record<string, AvatarConfigMaze>;
|
||||
MOC: Record<string, MOCConfigMaze>;
|
||||
AS: Record<string, ASConfigMaze>;
|
||||
PF: Record<string, PFConfigMaze>;
|
||||
};
|
||||
100
src/types/enka.ts
Normal file
100
src/types/enka.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
interface PrivacySettingInfo {
|
||||
displayCollection: boolean
|
||||
displayRecord: boolean
|
||||
displayRecordTeam: boolean
|
||||
displayOnlineStatus: boolean
|
||||
displayDiary: boolean
|
||||
}
|
||||
|
||||
interface ChallengeInfo {
|
||||
// Thêm các field cụ thể nếu có
|
||||
}
|
||||
|
||||
interface RecordInfo {
|
||||
achievementCount: number
|
||||
bookCount: number
|
||||
avatarCount: number
|
||||
equipmentCount: number
|
||||
musicCount: number
|
||||
relicCount: number
|
||||
challengeInfo: ChallengeInfo
|
||||
maxRogueChallengeScore: number
|
||||
}
|
||||
|
||||
|
||||
interface SubAffix {
|
||||
affixId: number
|
||||
cnt: number
|
||||
step?: number
|
||||
}
|
||||
|
||||
interface FlatProp {
|
||||
type: string
|
||||
value: number
|
||||
}
|
||||
|
||||
interface RelicFlat {
|
||||
props: FlatProp[]
|
||||
setName: string
|
||||
setID: 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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
20
src/types/filter.ts
Normal file
20
src/types/filter.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface FilterAvatarType {
|
||||
name: string;
|
||||
path: string[];
|
||||
element: string[];
|
||||
rarity: string[];
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export interface FilterLightconeType {
|
||||
path: string[];
|
||||
rarity: string[];
|
||||
locale: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
|
||||
export interface FilterRelicType {
|
||||
locale: string;
|
||||
name: string;
|
||||
}
|
||||
13
src/types/index.ts
Normal file
13
src/types/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export * from "./characterBasic"
|
||||
export * from "./characterDetail"
|
||||
export * from "./srtools"
|
||||
export * from "./filter"
|
||||
export * from "./mics"
|
||||
export * from "./config_maze"
|
||||
export * from "./lightconeBasic"
|
||||
export * from "./lightconeDetail"
|
||||
export * from "./relicBasic"
|
||||
export * from "./relicDetail"
|
||||
export * from "./affix"
|
||||
export * from "./enka"
|
||||
export * from "./card"
|
||||
17
src/types/lightconeBasic.ts
Normal file
17
src/types/lightconeBasic.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export interface LightConeBasicRaw {
|
||||
rank: string;
|
||||
baseType: string;
|
||||
en: string;
|
||||
desc: string;
|
||||
kr: string;
|
||||
cn: string;
|
||||
jp: string;
|
||||
}
|
||||
|
||||
export interface LightConeBasic {
|
||||
id: string;
|
||||
rank: string;
|
||||
baseType: string;
|
||||
desc: string;
|
||||
lang: Map<string, string>;
|
||||
}
|
||||
38
src/types/lightconeDetail.ts
Normal file
38
src/types/lightconeDetail.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
export interface LightConeDetail {
|
||||
Name: string;
|
||||
Desc: string;
|
||||
Rarity: string;
|
||||
BaseType: string;
|
||||
Refinements: RefinementDetail;
|
||||
Stats: StatEntryDetail[];
|
||||
}
|
||||
|
||||
interface RefinementDetail {
|
||||
Name: string;
|
||||
Desc: string;
|
||||
Level: Record<string, {
|
||||
ParamList: number[];
|
||||
}>;
|
||||
}
|
||||
|
||||
interface StatEntryDetail {
|
||||
EquipmentID: number;
|
||||
Promotion?: number;
|
||||
PromotionCostList: PromotionCost[];
|
||||
PlayerLevelRequire?: number;
|
||||
WorldLevelRequire?: number;
|
||||
MaxLevel: number;
|
||||
BaseHP: number;
|
||||
BaseHPAdd: number;
|
||||
BaseAttack: number;
|
||||
BaseAttackAdd: number;
|
||||
BaseDefence: number;
|
||||
BaseDefenceAdd: number;
|
||||
}
|
||||
|
||||
interface PromotionCost {
|
||||
$type: string;
|
||||
ItemID: number;
|
||||
ItemNum: number;
|
||||
Rarity: string;
|
||||
}
|
||||
71
src/types/mics.ts
Normal file
71
src/types/mics.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
export type AvatarDataStore = {
|
||||
rank: number,
|
||||
skills: { [key: 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 BattleConfigStore = {
|
||||
battle_type: string;
|
||||
blessings: BattleBuffStore[]
|
||||
custom_stats: SubAffixStore[];
|
||||
cycle_count: number;
|
||||
stage_id: number;
|
||||
path_resonance_id: number;
|
||||
monsters: MonsterStore[][];
|
||||
}
|
||||
28
src/types/relicBasic.ts
Normal file
28
src/types/relicBasic.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export interface RelicBasicRawEffect {
|
||||
en: string;
|
||||
ParamList: number[];
|
||||
kr: string;
|
||||
cn: string;
|
||||
jp: string;
|
||||
}
|
||||
|
||||
export interface RelicBasicRaw {
|
||||
icon: string;
|
||||
en: string;
|
||||
kr: string;
|
||||
cn: string;
|
||||
jp: string;
|
||||
set: Map<string, RelicBasicRawEffect>;
|
||||
}
|
||||
|
||||
export interface RelicBasicEffect {
|
||||
ParamList: number[];
|
||||
lang: Map<string, string>;
|
||||
}
|
||||
|
||||
export interface RelicBasic {
|
||||
id: string;
|
||||
icon: string;
|
||||
lang: Map<string, string>;
|
||||
set: Map<string, RelicBasicEffect>;
|
||||
}
|
||||
18
src/types/relicDetail.ts
Normal file
18
src/types/relicDetail.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface PartData {
|
||||
Name: string;
|
||||
Desc: string;
|
||||
Story: string;
|
||||
}
|
||||
|
||||
export interface RequireBonus {
|
||||
Desc: string;
|
||||
ParamList: number[];
|
||||
}
|
||||
|
||||
export interface RelicDetail {
|
||||
Name: string;
|
||||
Icon: string;
|
||||
Parts: Record<string, PartData>;
|
||||
RequireNum: Record<string, RequireBonus>;
|
||||
}
|
||||
|
||||
79
src/types/srtools.ts
Normal file
79
src/types/srtools.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
export interface SubAffix {
|
||||
sub_affix_id: number;
|
||||
count: number;
|
||||
step: number;
|
||||
}
|
||||
|
||||
export interface RelicJson {
|
||||
level: number;
|
||||
relic_id: number;
|
||||
relic_set_id: number;
|
||||
main_affix_id: number;
|
||||
sub_affixes: SubAffix[];
|
||||
internal_uid: number;
|
||||
equip_avatar: number;
|
||||
}
|
||||
|
||||
export interface LightconeJson {
|
||||
level: number;
|
||||
item_id: number;
|
||||
equip_avatar: number;
|
||||
rank: number;
|
||||
promotion: number;
|
||||
internal_uid: number;
|
||||
}
|
||||
export interface AvatarData {
|
||||
rank: number,
|
||||
skills: { [key: string]: number }
|
||||
}
|
||||
|
||||
export interface AvatarJson {
|
||||
owner_uid: number;
|
||||
avatar_id: number;
|
||||
data: AvatarData;
|
||||
level: number;
|
||||
promotion: number;
|
||||
techniques: number[];
|
||||
sp_value: number;
|
||||
sp_max: number;
|
||||
}
|
||||
export interface MonsterJson {
|
||||
monster_id: number;
|
||||
level: number;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface DynamicKeyJson {
|
||||
key: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
//BattleBuff
|
||||
export interface BattleBuffJson {
|
||||
level: number;
|
||||
id: number;
|
||||
dynamic_key?: DynamicKeyJson;
|
||||
}
|
||||
|
||||
export interface BattleConfigJson {
|
||||
battle_type: string;
|
||||
blessings: BattleBuffJson[]
|
||||
custom_stats: SubAffix[];
|
||||
cycle_count: number;
|
||||
stage_id: number;
|
||||
path_resonance_id: number;
|
||||
monsters: MonsterJson[][];
|
||||
}
|
||||
|
||||
export interface FreeSRJson {
|
||||
lightcones: LightconeJson[];
|
||||
relics: RelicJson[];
|
||||
avatars: { [key: string]: AvatarJson };
|
||||
battle_config: BattleConfigJson;
|
||||
}
|
||||
|
||||
export interface PSResponse {
|
||||
status: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
9
src/zod/affix.zod.ts
Normal file
9
src/zod/affix.zod.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
// Generated by ts-to-zod
|
||||
import { z } from "zod";
|
||||
|
||||
export const affixDetailSchema = z.object({
|
||||
property: z.string(),
|
||||
base: z.number(),
|
||||
step: z.number(),
|
||||
step_num: z.number(),
|
||||
});
|
||||
21
src/zod/card.zod.ts
Normal file
21
src/zod/card.zod.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
// Generated by ts-to-zod
|
||||
import { z } from "zod";
|
||||
|
||||
export const characterInfoCardTypeSchema = z.object({
|
||||
key: z.number(),
|
||||
avatar_id: z.number(),
|
||||
rank: z.number(),
|
||||
level: z.number(),
|
||||
lightcone: z.object({
|
||||
level: z.number(),
|
||||
rank: z.number(),
|
||||
item_id: z.number(),
|
||||
}),
|
||||
relics: z.array(
|
||||
z.object({
|
||||
level: z.number(),
|
||||
relic_id: z.number(),
|
||||
relic_set_id: z.number(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
15
src/zod/characterBasic.zod.ts
Normal file
15
src/zod/characterBasic.zod.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// Generated by ts-to-zod
|
||||
import { z } from "zod";
|
||||
|
||||
export const characterBasicRawSchema = z.object({
|
||||
release: z.number(),
|
||||
icon: z.string(),
|
||||
rank: z.string(),
|
||||
baseType: z.string(),
|
||||
damageType: z.string(),
|
||||
en: z.string(),
|
||||
desc: z.string(),
|
||||
kr: z.string(),
|
||||
cn: z.string(),
|
||||
jp: z.string(),
|
||||
});
|
||||
163
src/zod/characterDetail.zod.ts
Normal file
163
src/zod/characterDetail.zod.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
// Generated by ts-to-zod
|
||||
import { z } from "zod";
|
||||
|
||||
export const rankTypeSchema = z.object({
|
||||
Id: z.number(),
|
||||
Name: z.string(),
|
||||
Desc: z.string(),
|
||||
ParamList: z.array(z.number()),
|
||||
});
|
||||
|
||||
export const uniqueAbilitySchema = z.object({
|
||||
Tag: z.string(),
|
||||
Name: z.string(),
|
||||
Desc: z.string(),
|
||||
Param: z.array(z.number()),
|
||||
});
|
||||
|
||||
export const voiceActorsSchema = z.object({
|
||||
Chinese: z.string().nullable(),
|
||||
Japanese: z.string().nullable(),
|
||||
Korean: z.string().nullable(),
|
||||
English: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const levelParamsSchema = z.object({
|
||||
Level: z.number(),
|
||||
ParamList: z.array(z.number()),
|
||||
});
|
||||
|
||||
export const itemConfigRowSchema = z.object({
|
||||
$type: z.string(),
|
||||
ItemID: z.number(),
|
||||
ItemNum: z.number(),
|
||||
Rarity: z.string(),
|
||||
});
|
||||
|
||||
export const spriteSkillSchema = z.object({
|
||||
Name: z.string(),
|
||||
Desc: z.string().nullable(),
|
||||
Type: z.string().nullable(),
|
||||
Tag: z.string(),
|
||||
SPBase: z.number().nullable(),
|
||||
BPNeed: z.number(),
|
||||
BPAdd: z.number().nullable(),
|
||||
ShowStanceList: z.array(z.number()),
|
||||
SkillComboValueDelta: z.number().nullable(),
|
||||
Level: z.record(levelParamsSchema),
|
||||
});
|
||||
|
||||
export const statSchema = z.object({
|
||||
AttackBase: z.number(),
|
||||
AttackAdd: z.number(),
|
||||
DefenceBase: z.number(),
|
||||
DefenceAdd: z.number(),
|
||||
HPBase: z.number(),
|
||||
HPAdd: z.number(),
|
||||
SpeedBase: z.number(),
|
||||
CriticalChance: z.number(),
|
||||
CriticalDamage: z.number(),
|
||||
BaseAggro: z.number(),
|
||||
Cost: z.array(itemConfigRowSchema),
|
||||
});
|
||||
|
||||
export const relicRecommendPropertySchema = z.object({
|
||||
$type: z.string(),
|
||||
RelicType: z.string(),
|
||||
PropertyType: z.string(),
|
||||
});
|
||||
|
||||
export const characterInfoSchema = z.object({
|
||||
Camp: z.string().nullable(),
|
||||
VA: voiceActorsSchema,
|
||||
Stories: z.record(z.string().nullable()),
|
||||
Voicelines: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const skillTypeSchema = z.object({
|
||||
Id: z.number(),
|
||||
Name: z.string(),
|
||||
Desc: z.string(),
|
||||
Type: z.string(),
|
||||
Tag: z.string(),
|
||||
SPBase: z.number().nullable(),
|
||||
BPNeed: z.number(),
|
||||
BPAdd: z.number(),
|
||||
ShowStanceList: z.array(z.number()),
|
||||
SkillComboValueDelta: z.number().nullable(),
|
||||
Level: z.record(levelParamsSchema),
|
||||
});
|
||||
|
||||
export const skillTreePointSchema = z.object({
|
||||
Anchor: z.string(),
|
||||
AvatarPromotionLimit: z.number().nullable(),
|
||||
AvatarLevelLimit: z.number().nullable(),
|
||||
DefaultUnlock: z.boolean(),
|
||||
Icon: z.string(),
|
||||
LevelUpSkillID: z.array(z.number()),
|
||||
MaterialList: z.array(itemConfigRowSchema),
|
||||
MaxLevel: z.number(),
|
||||
ParamList: z.array(z.any()),
|
||||
PointID: z.number(),
|
||||
PointName: z.string().nullable(),
|
||||
PointDesc: z.string().nullable(),
|
||||
PointTriggerKey: z.number(),
|
||||
PointType: z.number(),
|
||||
PrePoint: z.array(z.string()),
|
||||
StatusAddList: z.array(z.any()),
|
||||
});
|
||||
|
||||
export const memospriteSchema = z.object({
|
||||
Name: z.string(),
|
||||
Icon: z.string(),
|
||||
HPBase: z.string(),
|
||||
HPInherit: z.string(),
|
||||
HPSkill: z.number().nullable(),
|
||||
SpeedBase: z.string(),
|
||||
SpeedInherit: z.string(),
|
||||
SpeedSkill: z.number(),
|
||||
Aggro: z.number(),
|
||||
Skills: z.record(spriteSkillSchema),
|
||||
Talent: z.record(z.any()),
|
||||
});
|
||||
|
||||
export const relicsSchema = z.object({
|
||||
AvatarID: z.number(),
|
||||
Set4IDList: z.array(z.number()),
|
||||
Set2IDList: z.array(z.number()),
|
||||
PropertyList3: z.array(z.string()),
|
||||
PropertyList4: z.array(z.string()),
|
||||
PropertyList5: z.array(z.string()),
|
||||
PropertyList6: z.array(z.string()),
|
||||
PropertyList: z.array(relicRecommendPropertySchema),
|
||||
SubAffixPropertyList: z.array(z.string()),
|
||||
ScoreRankList: z.array(z.number()),
|
||||
});
|
||||
|
||||
export const enhancedTypeSchema = z.object({
|
||||
Descs: z.array(z.string()),
|
||||
ChangeRankList: z.any(),
|
||||
ChangeSkillTreeList: z.any(),
|
||||
Ranks: z.record(rankTypeSchema),
|
||||
Skills: z.record(skillTypeSchema),
|
||||
SkillTrees: z.record(z.record(skillTreePointSchema)),
|
||||
});
|
||||
|
||||
export const characterDetailSchema = z.object({
|
||||
Name: z.string(),
|
||||
Desc: z.string(),
|
||||
CharaInfo: characterInfoSchema,
|
||||
Rarity: z.string(),
|
||||
AvatarVOTag: z.string(),
|
||||
SPNeed: z.number().nullable(),
|
||||
BaseType: z.string(),
|
||||
DamageType: z.string(),
|
||||
Ranks: z.record(rankTypeSchema),
|
||||
Skills: z.record(skillTypeSchema),
|
||||
SkillTrees: z.record(z.record(skillTreePointSchema)),
|
||||
Memosprite: memospriteSchema,
|
||||
Unique: z.record(uniqueAbilitySchema),
|
||||
Stats: z.record(statSchema),
|
||||
Relics: relicsSchema,
|
||||
Enhanced: z.record(enhancedTypeSchema),
|
||||
});
|
||||
28
src/zod/config_maze.zod.ts
Normal file
28
src/zod/config_maze.zod.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// Generated by ts-to-zod
|
||||
import { z } from "zod";
|
||||
|
||||
export const aSConfigMazeSchema = z.object({
|
||||
buff_1: z.array(z.number()),
|
||||
buff_2: z.array(z.number()),
|
||||
maze_buff: z.number(),
|
||||
});
|
||||
|
||||
export const pFConfigMazeSchema = z.object({
|
||||
buff: z.array(z.number()),
|
||||
maze_buff: z.number(),
|
||||
});
|
||||
|
||||
export const mOCConfigMazeSchema = z.object({
|
||||
maze_buff: z.number(),
|
||||
});
|
||||
|
||||
export const avatarConfigMazeSchema = z.object({
|
||||
maze_buff: z.array(z.number()),
|
||||
});
|
||||
|
||||
export const configMazeSchema = z.object({
|
||||
Avatar: z.record(avatarConfigMazeSchema),
|
||||
MOC: z.record(mOCConfigMazeSchema),
|
||||
AS: z.record(aSConfigMazeSchema),
|
||||
PF: z.record(pFConfigMazeSchema),
|
||||
});
|
||||
100
src/zod/enka.zod.ts
Normal file
100
src/zod/enka.zod.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
// Generated by ts-to-zod
|
||||
import { z } from "zod";
|
||||
|
||||
const privacySettingInfoSchema = z.object({
|
||||
displayCollection: z.boolean(),
|
||||
displayRecord: z.boolean(),
|
||||
displayRecordTeam: z.boolean(),
|
||||
displayOnlineStatus: z.boolean(),
|
||||
displayDiary: z.boolean(),
|
||||
});
|
||||
|
||||
const challengeInfoSchema = z.object({});
|
||||
|
||||
const recordInfoSchema = z.object({
|
||||
achievementCount: z.number(),
|
||||
bookCount: z.number(),
|
||||
avatarCount: z.number(),
|
||||
equipmentCount: z.number(),
|
||||
musicCount: z.number(),
|
||||
relicCount: z.number(),
|
||||
challengeInfo: challengeInfoSchema,
|
||||
maxRogueChallengeScore: z.number(),
|
||||
});
|
||||
|
||||
const subAffixSchema = z.object({
|
||||
affixId: z.number(),
|
||||
cnt: z.number(),
|
||||
step: z.number().optional(),
|
||||
});
|
||||
|
||||
const flatPropSchema = z.object({
|
||||
type: z.string(),
|
||||
value: z.number(),
|
||||
});
|
||||
|
||||
const relicFlatSchema = z.object({
|
||||
props: z.array(flatPropSchema),
|
||||
setName: z.string(),
|
||||
setID: z.number(),
|
||||
});
|
||||
|
||||
const relicSchema = z.object({
|
||||
mainAffixId: z.number(),
|
||||
subAffixList: z.array(subAffixSchema),
|
||||
tid: z.number(),
|
||||
type: z.number(),
|
||||
level: z.number(),
|
||||
_flat: relicFlatSchema,
|
||||
});
|
||||
|
||||
const skillTreeSchema = z.object({
|
||||
pointId: z.number(),
|
||||
level: z.number(),
|
||||
});
|
||||
|
||||
const equipmentFlatSchema = z.object({
|
||||
props: z.array(flatPropSchema),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
const equipmentSchema = z.object({
|
||||
rank: z.number(),
|
||||
tid: z.number(),
|
||||
promotion: z.number(),
|
||||
level: z.number(),
|
||||
_flat: equipmentFlatSchema,
|
||||
});
|
||||
|
||||
export const avatarEnkaDetailSchema = z.object({
|
||||
relicList: z.array(relicSchema),
|
||||
level: z.number(),
|
||||
promotion: z.number(),
|
||||
rank: z.number().optional(),
|
||||
skillTreeList: z.array(skillTreeSchema),
|
||||
equipment: equipmentSchema,
|
||||
avatarId: z.number(),
|
||||
_assist: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const detailInfoSchema = z.object({
|
||||
worldLevel: z.number(),
|
||||
privacySettingInfo: privacySettingInfoSchema,
|
||||
headIcon: z.number(),
|
||||
signature: z.string(),
|
||||
avatarDetailList: z.array(avatarEnkaDetailSchema),
|
||||
platform: z.string(),
|
||||
recordInfo: recordInfoSchema,
|
||||
uid: z.number(),
|
||||
level: z.number(),
|
||||
nickname: z.string(),
|
||||
isDisplayAvatar: z.boolean(),
|
||||
friendCount: z.number(),
|
||||
personalCardId: z.number(),
|
||||
});
|
||||
|
||||
export const enkaResponseSchema = z.object({
|
||||
detailInfo: detailInfoSchema,
|
||||
ttl: z.number(),
|
||||
uid: z.string(),
|
||||
});
|
||||
22
src/zod/filter.zod.ts
Normal file
22
src/zod/filter.zod.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// Generated by ts-to-zod
|
||||
import { z } from "zod";
|
||||
|
||||
export const filterAvatarTypeSchema = z.object({
|
||||
name: z.string(),
|
||||
path: z.array(z.string()),
|
||||
element: z.array(z.string()),
|
||||
rarity: z.string(),
|
||||
locale: z.string(),
|
||||
});
|
||||
|
||||
export const filterLightconeTypeSchema = z.object({
|
||||
path: z.array(z.string()),
|
||||
rarity: z.array(z.string()),
|
||||
locale: z.string(),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export const filterRelicTypeSchema = z.object({
|
||||
locale: z.string(),
|
||||
name: z.string(),
|
||||
});
|
||||
13
src/zod/index.ts
Normal file
13
src/zod/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export * from "./affix.zod";
|
||||
export * from "./card.zod";
|
||||
export * from "./characterBasic.zod";
|
||||
export * from "./characterDetail.zod";
|
||||
export * from "./config_maze.zod";
|
||||
export * from "./enka.zod";
|
||||
export * from "./filter.zod";
|
||||
export * from "./lightconeBasic.zod";
|
||||
export * from "./lightconeDetail.zod";
|
||||
export * from "./mics.zod";
|
||||
export * from "./relicBasic.zod";
|
||||
export * from "./relicDetail.zod";
|
||||
export * from "./srtools.zod";
|
||||
12
src/zod/lightconeBasic.zod.ts
Normal file
12
src/zod/lightconeBasic.zod.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
// Generated by ts-to-zod
|
||||
import { z } from "zod";
|
||||
|
||||
export const lightConeBasicRawSchema = z.object({
|
||||
rank: z.string(),
|
||||
baseType: z.string(),
|
||||
en: z.string(),
|
||||
desc: z.string(),
|
||||
kr: z.string(),
|
||||
cn: z.string(),
|
||||
jp: z.string(),
|
||||
});
|
||||
43
src/zod/lightconeDetail.zod.ts
Normal file
43
src/zod/lightconeDetail.zod.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Generated by ts-to-zod
|
||||
import { z } from "zod";
|
||||
|
||||
const refinementDetailSchema = z.object({
|
||||
Name: z.string(),
|
||||
Desc: z.string(),
|
||||
Level: z.record(
|
||||
z.object({
|
||||
ParamList: z.array(z.number()),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const promotionCostSchema = z.object({
|
||||
$type: z.string(),
|
||||
ItemID: z.number(),
|
||||
ItemNum: z.number(),
|
||||
Rarity: z.string(),
|
||||
});
|
||||
|
||||
const statEntryDetailSchema = z.object({
|
||||
EquipmentID: z.number(),
|
||||
Promotion: z.number().optional(),
|
||||
PromotionCostList: z.array(promotionCostSchema),
|
||||
PlayerLevelRequire: z.number().optional(),
|
||||
WorldLevelRequire: z.number().optional(),
|
||||
MaxLevel: z.number(),
|
||||
BaseHP: z.number(),
|
||||
BaseHPAdd: z.number(),
|
||||
BaseAttack: z.number(),
|
||||
BaseAttackAdd: z.number(),
|
||||
BaseDefence: z.number(),
|
||||
BaseDefenceAdd: z.number(),
|
||||
});
|
||||
|
||||
export const lightConeDetailSchema = z.object({
|
||||
Name: z.string(),
|
||||
Desc: z.string(),
|
||||
Rarity: z.string(),
|
||||
BaseType: z.string(),
|
||||
Refinements: refinementDetailSchema,
|
||||
Stats: z.array(statEntryDetailSchema),
|
||||
});
|
||||
82
src/zod/mics.zod.ts
Normal file
82
src/zod/mics.zod.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
// Generated by ts-to-zod
|
||||
import { z } from "zod";
|
||||
|
||||
export const avatarDataStoreSchema = z.object({
|
||||
rank: z.number(),
|
||||
skills: z.record(z.number()),
|
||||
});
|
||||
|
||||
export const lightconeStoreSchema = z.object({
|
||||
level: z.number(),
|
||||
item_id: z.number(),
|
||||
rank: z.number(),
|
||||
promotion: z.number(),
|
||||
});
|
||||
|
||||
export const subAffixStoreSchema = z.object({
|
||||
sub_affix_id: z.number(),
|
||||
count: z.number(),
|
||||
step: z.number(),
|
||||
});
|
||||
|
||||
export const relicStoreSchema = z.object({
|
||||
level: z.number(),
|
||||
relic_id: z.number(),
|
||||
relic_set_id: z.number(),
|
||||
main_affix_id: z.number(),
|
||||
sub_affixes: z.array(subAffixStoreSchema),
|
||||
});
|
||||
|
||||
export const avatarProfileStoreSchema = z.object({
|
||||
profile_name: z.string(),
|
||||
lightcone: lightconeStoreSchema.nullable(),
|
||||
relics: z.record(relicStoreSchema),
|
||||
});
|
||||
|
||||
export const avatarStoreSchema = z.object({
|
||||
owner_uid: z.number(),
|
||||
avatar_id: z.number(),
|
||||
data: avatarDataStoreSchema,
|
||||
level: z.number(),
|
||||
promotion: z.number(),
|
||||
techniques: z.array(z.number()),
|
||||
sp_value: z.number(),
|
||||
sp_max: z.number(),
|
||||
can_change_sp: z.boolean(),
|
||||
enhanced: z.string(),
|
||||
profileSelect: z.number(),
|
||||
profileList: z.array(avatarProfileStoreSchema),
|
||||
});
|
||||
|
||||
export const monsterStoreSchema = z.object({
|
||||
monster_id: z.number(),
|
||||
level: z.number(),
|
||||
amount: z.number(),
|
||||
});
|
||||
|
||||
export const dynamicKeyStoreSchema = z.object({
|
||||
key: z.string(),
|
||||
value: z.number(),
|
||||
});
|
||||
|
||||
export const battleBuffStoreSchema = z.object({
|
||||
level: z.number(),
|
||||
id: z.number(),
|
||||
dynamic_key: dynamicKeyStoreSchema.optional(),
|
||||
});
|
||||
|
||||
export const battleConfigStoreSchema = z.object({
|
||||
battle_type: z.string(),
|
||||
blessings: z.array(battleBuffStoreSchema),
|
||||
custom_stats: z.array(subAffixStoreSchema),
|
||||
cycle_count: z.number(),
|
||||
stage_id: z.number(),
|
||||
path_resonance_id: z.number(),
|
||||
monsters: z.array(z.array(monsterStoreSchema)),
|
||||
});
|
||||
|
||||
|
||||
export const micsSchema = z.object({
|
||||
avatars: z.record(z.string(), avatarStoreSchema),
|
||||
battle_config: battleConfigStoreSchema,
|
||||
})
|
||||
10
src/zod/relicBasic.zod.ts
Normal file
10
src/zod/relicBasic.zod.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// Generated by ts-to-zod
|
||||
import { z } from "zod";
|
||||
|
||||
export const relicBasicRawEffectSchema = z.object({
|
||||
en: z.string(),
|
||||
ParamList: z.array(z.number()),
|
||||
kr: z.string(),
|
||||
cn: z.string(),
|
||||
jp: z.string(),
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user