Refactor TypeScript interfaces and types for consistency and readability
Gitea Auto Deploy / Deploy-Container (push) Successful in 49s
Gitea Auto Deploy / Deploy-Container (push) Successful in 49s
- Reformatted multiple TypeScript interfaces across various files to ensure consistent spacing and indentation. - Updated `changelog.ts`, `enka.ts`, `extraData.ts`, `gloval.d.ts`, `index.ts`, `lightconeDetail.ts`, `metaData.ts`, `mics.ts`, `modelConfig.ts`, `monsterDetail.ts`, `peakDetail.ts`, `pfDetail.ts`, `psConnect.ts`, `relicDetail.ts`, `showcase.ts`, `srtools.ts`, and `zod/index.ts` for improved code clarity. - Ensured all interfaces are properly formatted with consistent line breaks and spacing. - Made minor adjustments to the `tsconfig.json` file for formatting consistency.
This commit is contained in:
+265
-265
@@ -1,74 +1,74 @@
|
||||
"use client"
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { useState, useMemo } from "react";
|
||||
import useCopyProfileStore from "@/stores/copyProfile";
|
||||
import ProfileCard from "../card/profileCard";
|
||||
import { AvatarProfileCardType, AvatarProfileStore } from "@/types";
|
||||
import Image from "next/image";
|
||||
import { getNameChar, calcRarity } from "@/helper";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import { useTranslations } from "next-intl";
|
||||
import SelectCustomImage from "../select/customSelectImage";
|
||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
|
||||
export default function CopyImport() {
|
||||
const { avatars, setAvatar } = useUserDataStore();
|
||||
const { avatarSelected } = useCurrentDataStore()
|
||||
const { mapAvatar, baseType, damageType } = useDetailDataStore()
|
||||
const { locale } = useLocaleStore()
|
||||
const {
|
||||
selectedProfiles,
|
||||
avatarCopySelected,
|
||||
setSelectedProfiles,
|
||||
setAvatarCopySelected,
|
||||
listElement,
|
||||
listPath,
|
||||
listRank,
|
||||
setListElement,
|
||||
setListPath,
|
||||
setListRank
|
||||
} = useCopyProfileStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
|
||||
const [message, setMessage] = useState({
|
||||
type: "",
|
||||
text: ""
|
||||
})
|
||||
|
||||
const listAvatar = useMemo(() => {
|
||||
if (!mapAvatar || !locale || !transI18n) return []
|
||||
let list = Object.values(mapAvatar);
|
||||
const allElementFalse = !Object.values(listElement).some(v => v)
|
||||
const allPathFalse = !Object.values(listPath).some(v => v)
|
||||
const allRarityFalse = !Object.values(listRank).some(v => v)
|
||||
list = list.filter(item => (allElementFalse || listElement[item.DamageType]) && (allPathFalse || listPath[item.BaseType]) && (allRarityFalse || listRank[calcRarity(item.Rarity)]))
|
||||
list.sort((a, b) => {
|
||||
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
|
||||
if (r !== 0) return r
|
||||
return a.ID - b.ID
|
||||
})
|
||||
return list
|
||||
}, [mapAvatar, listElement, listPath, listRank, locale, transI18n])
|
||||
|
||||
|
||||
const handleProfileToggle = (profile: AvatarProfileCardType) => {
|
||||
if (selectedProfiles.some((selectedProfile) => selectedProfile.key === profile.key)) {
|
||||
setSelectedProfiles(selectedProfiles.filter((selectedProfile) => selectedProfile.key !== profile.key));
|
||||
return;
|
||||
}
|
||||
setSelectedProfiles([...selectedProfiles, profile]);
|
||||
};
|
||||
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedProfiles([]);
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("clearAll")
|
||||
})
|
||||
};
|
||||
|
||||
"use client"
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { useState, useMemo } from "react";
|
||||
import useCopyProfileStore from "@/stores/copyProfile";
|
||||
import ProfileCard from "../card/profileCard";
|
||||
import { AvatarProfileCardType, AvatarProfileStore } from "@/types";
|
||||
import Image from "next/image";
|
||||
import { getNameChar, calcRarity } from "@/helper";
|
||||
import useLocaleStore from "@/stores/localeStore";
|
||||
import { useTranslations } from "next-intl";
|
||||
import SelectCustomImage from "../select/customSelectImage";
|
||||
import useCurrentDataStore from "@/stores/currentDataStore";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
|
||||
export default function CopyImport() {
|
||||
const { avatars, setAvatar } = useUserDataStore();
|
||||
const { avatarSelected } = useCurrentDataStore()
|
||||
const { mapAvatar, baseType, damageType } = useDetailDataStore()
|
||||
const { locale } = useLocaleStore()
|
||||
const {
|
||||
selectedProfiles,
|
||||
avatarCopySelected,
|
||||
setSelectedProfiles,
|
||||
setAvatarCopySelected,
|
||||
listElement,
|
||||
listPath,
|
||||
listRank,
|
||||
setListElement,
|
||||
setListPath,
|
||||
setListRank
|
||||
} = useCopyProfileStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
|
||||
const [message, setMessage] = useState({
|
||||
type: "",
|
||||
text: ""
|
||||
})
|
||||
|
||||
const listAvatar = useMemo(() => {
|
||||
if (!mapAvatar || !locale || !transI18n) return []
|
||||
let list = Object.values(mapAvatar);
|
||||
const allElementFalse = !Object.values(listElement).some(v => v)
|
||||
const allPathFalse = !Object.values(listPath).some(v => v)
|
||||
const allRarityFalse = !Object.values(listRank).some(v => v)
|
||||
list = list.filter(item => (allElementFalse || listElement[item.DamageType]) && (allPathFalse || listPath[item.BaseType]) && (allRarityFalse || listRank[calcRarity(item.Rarity)]))
|
||||
list.sort((a, b) => {
|
||||
const r = calcRarity(b.Rarity) - calcRarity(a.Rarity)
|
||||
if (r !== 0) return r
|
||||
return a.ID - b.ID
|
||||
})
|
||||
return list
|
||||
}, [mapAvatar, listElement, listPath, listRank, locale, transI18n])
|
||||
|
||||
|
||||
const handleProfileToggle = (profile: AvatarProfileCardType) => {
|
||||
if (selectedProfiles.some((selectedProfile) => selectedProfile.key === profile.key)) {
|
||||
setSelectedProfiles(selectedProfiles.filter((selectedProfile) => selectedProfile.key !== profile.key));
|
||||
return;
|
||||
}
|
||||
setSelectedProfiles([...selectedProfiles, profile]);
|
||||
};
|
||||
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedProfiles([]);
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("clearAll")
|
||||
})
|
||||
};
|
||||
|
||||
const selectAll = () => {
|
||||
if (avatarCopySelected) {
|
||||
const sourceAvatar = avatars[avatarCopySelected.ID.toString()]
|
||||
@@ -79,32 +79,32 @@ export default function CopyImport() {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
key: index,
|
||||
...profile,
|
||||
} as AvatarProfileCardType
|
||||
}).filter((profile) => profile !== null));
|
||||
}
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("selectAll")
|
||||
})
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
if (selectedProfiles.length === 0) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: transI18n("pleaseSelectAtLeastOneProfile")
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!avatarCopySelected) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: transI18n("noAvatarToCopySelected")
|
||||
});
|
||||
return;
|
||||
}
|
||||
key: index,
|
||||
...profile,
|
||||
} as AvatarProfileCardType
|
||||
}).filter((profile) => profile !== null));
|
||||
}
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("selectAll")
|
||||
})
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
if (selectedProfiles.length === 0) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: transI18n("pleaseSelectAtLeastOneProfile")
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!avatarCopySelected) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
text: transI18n("noAvatarToCopySelected")
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!avatarSelected) {
|
||||
setMessage({
|
||||
type: "error",
|
||||
@@ -130,180 +130,180 @@ export default function CopyImport() {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...profile,
|
||||
profile_name: profile.profile_name + ` - Copy: ${avatarCopySelected?.ID}`,
|
||||
} as AvatarProfileStore
|
||||
}).filter((profile) => profile !== null);
|
||||
...profile,
|
||||
profile_name: profile.profile_name + ` - Copy: ${avatarCopySelected?.ID}`,
|
||||
} as AvatarProfileStore
|
||||
}).filter((profile) => profile !== null);
|
||||
|
||||
const newAvatar = {
|
||||
...targetAvatar,
|
||||
profileList: targetAvatar.profileList.concat(newListProfile),
|
||||
profileSelect: targetAvatar.profileList.length + newListProfile.length - 1,
|
||||
}
|
||||
setAvatar(newAvatar);
|
||||
setSelectedProfiles([]);
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("copied")
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg px-2 min-h-[60vh]">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-2">
|
||||
|
||||
<div className="mt-2 w-full">
|
||||
|
||||
<div className="flex items-start flex-col gap-2 mt-2 w-full">
|
||||
<div>{transI18n("filter")}</div>
|
||||
<div className="flex flex-wrap justify-start gap-5 md:gap-10 mt-2 w-full">
|
||||
{/* Path */}
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2 justify-start items-center">
|
||||
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => {
|
||||
setListPath({ ...listPath, [key]: !listPath[key] })
|
||||
}}
|
||||
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||
alt={key}
|
||||
className="h-8 w-8 object-contain rounded-md"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Element */}
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2 justify-start items-center">
|
||||
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => {
|
||||
setListElement({ ...listElement, [key]: !listElement[key] })
|
||||
}}
|
||||
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: listElement[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||
alt={key}
|
||||
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rank */}
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2 justify-end items-center">
|
||||
{Object.entries(listRank).map(([key], index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setListRank({ ...listRank, [key]: !listRank[key] })
|
||||
}}
|
||||
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: listRank[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">{key}*</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
|
||||
{listAvatar.length > 0 && (
|
||||
<div>
|
||||
<div>{transI18n("characterName")}</div>
|
||||
<SelectCustomImage
|
||||
customSet={listAvatar.map((avatar) => ({
|
||||
value: avatar.ID.toString(),
|
||||
label: getNameChar(locale, transI18n, avatar),
|
||||
imageUrl: `${process.env.CDN_URL}/${avatar.Image.AvatarIconPath}`
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={avatarCopySelected?.ID.toString() || ""}
|
||||
placeholder="Character Select"
|
||||
setSelectedCustomSet={(value) => setAvatarCopySelected(mapAvatar[value] || null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Selection Info */}
|
||||
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
|
||||
<span className="text-blue-400 font-medium">
|
||||
{transI18n("selectedProfiles")}: {selectedProfiles.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
|
||||
>
|
||||
{transI18n("clearAll")}
|
||||
</button>
|
||||
<button
|
||||
onClick={selectAll}
|
||||
className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
|
||||
{transI18n("selectAll")}
|
||||
</button>
|
||||
{selectedProfiles.length > 0 && (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="text-success/75 hover:text-success text-sm font-medium px-3 py-1 border border-success/75 rounded hover:bg-success/10 transition-colors cursor-pointer">
|
||||
{transI18n("copy")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{message.type && message.text && (
|
||||
<div className={(message.type == "error" ? "text-error" : "text-success") + " text-lg mt-2"} >{message.type == "error" ? "😭" : "🎉"} {message.text}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Character Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
setAvatar(newAvatar);
|
||||
setSelectedProfiles([]);
|
||||
setMessage({
|
||||
type: "success",
|
||||
text: transI18n("copied")
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg px-2 min-h-[60vh]">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-2">
|
||||
|
||||
<div className="mt-2 w-full">
|
||||
|
||||
<div className="flex items-start flex-col gap-2 mt-2 w-full">
|
||||
<div>{transI18n("filter")}</div>
|
||||
<div className="flex flex-wrap justify-start gap-5 md:gap-10 mt-2 w-full">
|
||||
{/* Path */}
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2 justify-start items-center">
|
||||
{Object.entries(baseType).filter(([key]) => key !== "").map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => {
|
||||
setListPath({ ...listPath, [key]: !listPath[key] })
|
||||
}}
|
||||
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: listPath[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||
alt={key}
|
||||
className="h-8 w-8 object-contain rounded-md"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Element */}
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2 justify-start items-center">
|
||||
{Object.entries(damageType).filter(([key]) => key !== "").map(([key, value]) => (
|
||||
<div
|
||||
key={key}
|
||||
onClick={() => {
|
||||
setListElement({ ...listElement, [key]: !listElement[key] })
|
||||
}}
|
||||
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: listElement[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<Image
|
||||
unoptimized
|
||||
crossOrigin="anonymous"
|
||||
src={`${process.env.CDN_URL}/${value.Icon}`}
|
||||
alt={key}
|
||||
className="h-7 w-7 2xl:h-10 2xl:w-10 object-contain rounded-md"
|
||||
width={200}
|
||||
height={200}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Rank */}
|
||||
<div>
|
||||
<div className="flex flex-wrap gap-2 justify-end items-center">
|
||||
{Object.entries(listRank).map(([key], index) => (
|
||||
<div
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setListRank({ ...listRank, [key]: !listRank[key] })
|
||||
}}
|
||||
className="w-12.5 h-12.5 hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
|
||||
style={{
|
||||
backgroundColor: listRank[key] ? "#374151" : "#6B7280"
|
||||
}}>
|
||||
<div className="font-bold text-white h-8 w-8 text-center flex items-center justify-center">{key}*</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
|
||||
{listAvatar.length > 0 && (
|
||||
<div>
|
||||
<div>{transI18n("characterName")}</div>
|
||||
<SelectCustomImage
|
||||
customSet={listAvatar.map((avatar) => ({
|
||||
value: avatar.ID.toString(),
|
||||
label: getNameChar(locale, transI18n, avatar),
|
||||
imageUrl: `${process.env.CDN_URL}/${avatar.Image.AvatarIconPath}`
|
||||
}))}
|
||||
excludeSet={[]}
|
||||
selectedCustomSet={avatarCopySelected?.ID.toString() || ""}
|
||||
placeholder="Character Select"
|
||||
setSelectedCustomSet={(value) => setAvatarCopySelected(mapAvatar[value] || null)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{/* Selection Info */}
|
||||
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
|
||||
<span className="text-blue-400 font-medium">
|
||||
{transI18n("selectedProfiles")}: {selectedProfiles.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
|
||||
>
|
||||
{transI18n("clearAll")}
|
||||
</button>
|
||||
<button
|
||||
onClick={selectAll}
|
||||
className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
|
||||
{transI18n("selectAll")}
|
||||
</button>
|
||||
{selectedProfiles.length > 0 && (
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="text-success/75 hover:text-success text-sm font-medium px-3 py-1 border border-success/75 rounded hover:bg-success/10 transition-colors cursor-pointer">
|
||||
{transI18n("copy")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{message.type && message.text && (
|
||||
<div className={(message.type == "error" ? "text-error" : "text-success") + " text-lg mt-2"} >{message.type == "error" ? "😭" : "🎉"} {message.text}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Character Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{avatarCopySelected && avatars[avatarCopySelected?.ID.toString()]?.profileList.map((profile, index) => {
|
||||
if (!profile.lightcone?.item_id && Object.keys(profile.relics ?? {}).length == 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ProfileCard
|
||||
key={index}
|
||||
profile={{ ...profile, key: index }}
|
||||
selectedProfile={selectedProfiles}
|
||||
onProfileToggle={handleProfileToggle}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ProfileCard
|
||||
key={index}
|
||||
profile={{ ...profile, key: index }}
|
||||
selectedProfile={selectedProfiles}
|
||||
onProfileToggle={handleProfileToggle}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
+124
-124
@@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
import { useState } from "react";
|
||||
import CharacterInfoCard from "../card/characterInfoCard";
|
||||
import useEnkaStore from "@/stores/enkaStore";
|
||||
import { SendDataThroughProxy } from "@/lib/api/api";
|
||||
import { CharacterInfoCardType, EnkaResponse } from "@/types";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
"use client"
|
||||
import { useState } from "react";
|
||||
import CharacterInfoCard from "../card/characterInfoCard";
|
||||
import useEnkaStore from "@/stores/enkaStore";
|
||||
import { SendDataThroughProxy } from "@/lib/api/api";
|
||||
import { CharacterInfoCardType, EnkaResponse } from "@/types";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { converterOneEnkaDataToAvatarStore } from "@/helper";
|
||||
import useModelStore from "@/stores/modelStore";
|
||||
import { toast } from "react-toastify";
|
||||
@@ -31,24 +31,24 @@ const toCharacterInfoCard = (character: AvatarEnkaDetailInput): CharacterInfoCar
|
||||
relic_set_id: parseInt(relic.tid.toString().slice(1, -1), 10),
|
||||
})),
|
||||
});
|
||||
|
||||
export default function EnkaImport() {
|
||||
const {
|
||||
uidInput,
|
||||
setUidInput,
|
||||
enkaData,
|
||||
selectedCharacters,
|
||||
setSelectedCharacters,
|
||||
setEnkaData,
|
||||
} = useEnkaStore();
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { avatars, setAvatar } = useUserDataStore();
|
||||
|
||||
export default function EnkaImport() {
|
||||
const {
|
||||
uidInput,
|
||||
setUidInput,
|
||||
enkaData,
|
||||
selectedCharacters,
|
||||
setSelectedCharacters,
|
||||
setEnkaData,
|
||||
} = useEnkaStore();
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const { avatars, setAvatar } = useUserDataStore();
|
||||
const { setIsOpenImport } = useModelStore()
|
||||
const { mapAvatar } = useDetailDataStore()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [Error, setError] = useState("")
|
||||
const validCharacters = enkaData?.detailInfo.avatarDetailList.filter((character) => mapAvatar?.[character.avatarId]) ?? []
|
||||
|
||||
|
||||
const handlerFetchData = async () => {
|
||||
if (!uidInput) {
|
||||
setError(transI18n("pleaseEnterUid"))
|
||||
@@ -77,36 +77,36 @@ export default function EnkaImport() {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCharacterToggle = (character: CharacterInfoCardType) => {
|
||||
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
|
||||
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
|
||||
return;
|
||||
}
|
||||
setSelectedCharacters([...selectedCharacters, character]);
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedCharacters([]);
|
||||
};
|
||||
|
||||
|
||||
const handleCharacterToggle = (character: CharacterInfoCardType) => {
|
||||
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
|
||||
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
|
||||
return;
|
||||
}
|
||||
setSelectedCharacters([...selectedCharacters, character]);
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedCharacters([]);
|
||||
};
|
||||
|
||||
const selectAll = () => {
|
||||
if (enkaData) {
|
||||
setSelectedCharacters(validCharacters.map(toCharacterInfoCard));
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
if (selectedCharacters.length === 0) {
|
||||
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
|
||||
return;
|
||||
}
|
||||
if (!enkaData) {
|
||||
setError(transI18n("noDataToImport"));
|
||||
return;
|
||||
}
|
||||
setError("");
|
||||
const listAvatars = { ...avatars }
|
||||
|
||||
const handleImport = () => {
|
||||
if (selectedCharacters.length === 0) {
|
||||
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
|
||||
return;
|
||||
}
|
||||
if (!enkaData) {
|
||||
setError(transI18n("noDataToImport"));
|
||||
return;
|
||||
}
|
||||
setError("");
|
||||
const listAvatars = { ...avatars }
|
||||
const parsed = enkaResponseSchema.safeParse(enkaData)
|
||||
if (!parsed.success) {
|
||||
setError(transI18n("failedToFetchEnkaData"));
|
||||
@@ -125,80 +125,80 @@ export default function EnkaImport() {
|
||||
acc[skill.pointId] = skill.level;
|
||||
return acc;
|
||||
}, {} as Record<string, number>),
|
||||
}
|
||||
const newProfile = converterOneEnkaDataToAvatarStore(character, newAvatar.profileList.length)
|
||||
if (newProfile) {
|
||||
newAvatar.profileList.push(newProfile)
|
||||
newAvatar.profileSelect = newAvatar.profileList.length - 1
|
||||
}
|
||||
setAvatar(newAvatar)
|
||||
}
|
||||
|
||||
})
|
||||
setIsOpenImport(false)
|
||||
toast.success(transI18n("importEnkaDataSuccess"))
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-slate-900 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-white text-2xl font-bold mb-6">HSR UID</h1>
|
||||
|
||||
<div className="flex gap-4 items-center mb-6">
|
||||
<input
|
||||
type="text"
|
||||
value={uidInput}
|
||||
onChange={(e) => setUidInput(e.target.value)}
|
||||
className="bg-slate-800 text-white px-4 py-3 rounded-lg border border-slate-700 flex-1 max-w-md focus:outline-none focus:border-blue-500"
|
||||
placeholder="Enter UID"
|
||||
/>
|
||||
<button onClick={handlerFetchData} className="btn btn-success rounded-lg transition-colors">
|
||||
{transI18n("getData")}
|
||||
</button>
|
||||
{selectedCharacters.length > 0 && (
|
||||
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
|
||||
{transI18n("import")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{Error && (
|
||||
<div className="text-error text-base mt-2">😭 {Error}</div>
|
||||
)}
|
||||
|
||||
{/* Player Info */}
|
||||
{enkaData && (
|
||||
<div className="text-white space-y-2">
|
||||
<div className="text-lg">Name: {enkaData.detailInfo.nickname}</div>
|
||||
<div className="text-lg">UID: {enkaData.detailInfo.uid}</div>
|
||||
<div className="text-lg">Level: {enkaData.detailInfo.level}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selection Info */}
|
||||
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
|
||||
<span className="text-blue-400 font-medium">
|
||||
{transI18n("selectedCharacters")}: {selectedCharacters.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
|
||||
>
|
||||
{transI18n("clearAll")}
|
||||
</button>
|
||||
<button onClick={selectAll} className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
|
||||
{transI18n("selectAll")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
|
||||
</div>
|
||||
)}
|
||||
{/* Character Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
}
|
||||
const newProfile = converterOneEnkaDataToAvatarStore(character, newAvatar.profileList.length)
|
||||
if (newProfile) {
|
||||
newAvatar.profileList.push(newProfile)
|
||||
newAvatar.profileSelect = newAvatar.profileList.length - 1
|
||||
}
|
||||
setAvatar(newAvatar)
|
||||
}
|
||||
|
||||
})
|
||||
setIsOpenImport(false)
|
||||
toast.success(transI18n("importEnkaDataSuccess"))
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-slate-900 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-white text-2xl font-bold mb-6">HSR UID</h1>
|
||||
|
||||
<div className="flex gap-4 items-center mb-6">
|
||||
<input
|
||||
type="text"
|
||||
value={uidInput}
|
||||
onChange={(e) => setUidInput(e.target.value)}
|
||||
className="bg-slate-800 text-white px-4 py-3 rounded-lg border border-slate-700 flex-1 max-w-md focus:outline-none focus:border-blue-500"
|
||||
placeholder="Enter UID"
|
||||
/>
|
||||
<button onClick={handlerFetchData} className="btn btn-success rounded-lg transition-colors">
|
||||
{transI18n("getData")}
|
||||
</button>
|
||||
{selectedCharacters.length > 0 && (
|
||||
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
|
||||
{transI18n("import")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{Error && (
|
||||
<div className="text-error text-base mt-2">😭 {Error}</div>
|
||||
)}
|
||||
|
||||
{/* Player Info */}
|
||||
{enkaData && (
|
||||
<div className="text-white space-y-2">
|
||||
<div className="text-lg">Name: {enkaData.detailInfo.nickname}</div>
|
||||
<div className="text-lg">UID: {enkaData.detailInfo.uid}</div>
|
||||
<div className="text-lg">Level: {enkaData.detailInfo.level}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Selection Info */}
|
||||
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
|
||||
<span className="text-blue-400 font-medium">
|
||||
{transI18n("selectedCharacters")}: {selectedCharacters.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
|
||||
>
|
||||
{transI18n("clearAll")}
|
||||
</button>
|
||||
<button onClick={selectAll} className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
|
||||
{transI18n("selectAll")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
|
||||
</div>
|
||||
)}
|
||||
{/* Character Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{validCharacters.map((character) => (
|
||||
<CharacterInfoCard
|
||||
key={character.avatarId}
|
||||
@@ -206,9 +206,9 @@ export default function EnkaImport() {
|
||||
selectedCharacters={selectedCharacters}
|
||||
onCharacterToggle={handleCharacterToggle}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+134
-134
@@ -1,15 +1,15 @@
|
||||
"use client"
|
||||
|
||||
import useFreeSRStore from "@/stores/freesrStore";
|
||||
import useModelStore from "@/stores/modelStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { CharacterInfoCardType } from "@/types";
|
||||
import { useState } from "react";
|
||||
import CharacterInfoCard from "../card/characterInfoCard";
|
||||
import { freeSrJsonSchema } from "@/zod";
|
||||
import { toast } from "react-toastify";
|
||||
import { converterOneFreeSRDataToAvatarStore } from "@/helper";
|
||||
import { useTranslations } from "next-intl";
|
||||
"use client"
|
||||
|
||||
import useFreeSRStore from "@/stores/freesrStore";
|
||||
import useModelStore from "@/stores/modelStore";
|
||||
import useUserDataStore from "@/stores/userDataStore";
|
||||
import { CharacterInfoCardType } from "@/types";
|
||||
import { useState } from "react";
|
||||
import CharacterInfoCard from "../card/characterInfoCard";
|
||||
import { freeSrJsonSchema } from "@/zod";
|
||||
import { toast } from "react-toastify";
|
||||
import { converterOneFreeSRDataToAvatarStore } from "@/helper";
|
||||
import { useTranslations } from "next-intl";
|
||||
import useDetailDataStore from "@/stores/detailDataStore";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -36,55 +36,55 @@ const toCharacterInfoCard = (data: FreeSRJsonInput, character: FreeSRJsonInput["
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export default function FreeSRImport() {
|
||||
const { avatars, setAvatar } = useUserDataStore();
|
||||
const { mapAvatar } = useDetailDataStore();
|
||||
const { setIsOpenImport } = useModelStore()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
export default function FreeSRImport() {
|
||||
const { avatars, setAvatar } = useUserDataStore();
|
||||
const { mapAvatar } = useDetailDataStore();
|
||||
const { setIsOpenImport } = useModelStore()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [Error, setError] = useState("")
|
||||
const { freeSRData, setFreeSRData, selectedCharacters, setSelectedCharacters } = useFreeSRStore()
|
||||
const transI18n = useTranslations("DataPage")
|
||||
const parsedFreeSRData = freeSrJsonSchema.safeParse(freeSRData)
|
||||
const validFreeSRData = parsedFreeSRData.success ? parsedFreeSRData.data : null
|
||||
|
||||
const handleCharacterToggle = (character: CharacterInfoCardType) => {
|
||||
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
|
||||
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
|
||||
return;
|
||||
}
|
||||
setSelectedCharacters([...selectedCharacters, character]);
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedCharacters([]);
|
||||
};
|
||||
|
||||
|
||||
const handleCharacterToggle = (character: CharacterInfoCardType) => {
|
||||
if (selectedCharacters.some((selectedCharacter) => selectedCharacter.key === character.key)) {
|
||||
setSelectedCharacters(selectedCharacters.filter((selectedCharacter) => selectedCharacter.key !== character.key));
|
||||
return;
|
||||
}
|
||||
setSelectedCharacters([...selectedCharacters, character]);
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedCharacters([]);
|
||||
};
|
||||
|
||||
const selectAll = () => {
|
||||
const parsed = freeSrJsonSchema.safeParse(freeSRData)
|
||||
if (parsed.success) {
|
||||
setSelectedCharacters(Object.values(parsed.data.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => toCharacterInfoCard(parsed.data, character)));
|
||||
}
|
||||
};
|
||||
|
||||
const handlerReadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsLoading(true)
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) {
|
||||
setSelectedCharacters([])
|
||||
setFreeSRData(null)
|
||||
setError(transI18n("pleaseSelectAFile"))
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
if (!file.name.endsWith(".json") || file.type !== "application/json") {
|
||||
setSelectedCharacters([])
|
||||
setFreeSRData(null)
|
||||
setError(transI18n("fileMustBeAValidJsonFile"))
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const handlerReadFile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setIsLoading(true)
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) {
|
||||
setSelectedCharacters([])
|
||||
setFreeSRData(null)
|
||||
setError(transI18n("pleaseSelectAFile"))
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
if (!file.name.endsWith(".json") || file.type !== "application/json") {
|
||||
setSelectedCharacters([])
|
||||
setFreeSRData(null)
|
||||
setError(transI18n("fileMustBeAValidJsonFile"))
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
if (file) {
|
||||
|
||||
const reader = new FileReader();
|
||||
@@ -121,18 +121,18 @@ export default function FreeSRImport() {
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImport = () => {
|
||||
if (selectedCharacters.length === 0) {
|
||||
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
|
||||
return;
|
||||
}
|
||||
if (!freeSRData) {
|
||||
setError(transI18n("noDataToImport"));
|
||||
return;
|
||||
}
|
||||
setError("");
|
||||
|
||||
|
||||
const handleImport = () => {
|
||||
if (selectedCharacters.length === 0) {
|
||||
setError(transI18n("pleaseSelectAtLeastOneCharacter"));
|
||||
return;
|
||||
}
|
||||
if (!freeSRData) {
|
||||
setError(transI18n("noDataToImport"));
|
||||
return;
|
||||
}
|
||||
setError("");
|
||||
|
||||
const parsed = freeSrJsonSchema.safeParse(freeSRData)
|
||||
if (!parsed.success) {
|
||||
setError(transI18n("fileMustBeAValidJsonFile"));
|
||||
@@ -144,73 +144,73 @@ export default function FreeSRImport() {
|
||||
filterData.forEach((character) => {
|
||||
const newAvatar = { ...listAvatars[character.avatar_id] }
|
||||
if (Object.keys(newAvatar).length !== 0) {
|
||||
newAvatar.level = character.level
|
||||
newAvatar.promotion = character.promotion
|
||||
newAvatar.data = {
|
||||
rank: character.data.rank ?? 0,
|
||||
skills: character.data.skills
|
||||
}
|
||||
newAvatar.level = character.level
|
||||
newAvatar.promotion = character.promotion
|
||||
newAvatar.data = {
|
||||
rank: character.data.rank ?? 0,
|
||||
skills: character.data.skills
|
||||
}
|
||||
const newProfile = converterOneFreeSRDataToAvatarStore(parsed.data, newAvatar.profileList.length, character.avatar_id)
|
||||
if (newProfile) {
|
||||
newAvatar.profileList.push(newProfile)
|
||||
newAvatar.profileSelect = newAvatar.profileList.length - 1
|
||||
}
|
||||
setAvatar(newAvatar)
|
||||
}
|
||||
|
||||
})
|
||||
setIsOpenImport(false)
|
||||
toast.success(transI18n("importFreeSRDataSuccess"))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-slate-900 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-white text-2xl font-bold mb-6">{transI18n("freeSRImport")}</h1>
|
||||
|
||||
<div className="flex gap-4 items-center mb-6">
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">{transI18n("pickAFile")}</legend>
|
||||
<input type="file" accept=".json" className="file-input file-input-info" onChange={handlerReadFile} />
|
||||
<label className="label">{transI18n("onlySupportFreeSRJsonFile")}</label>
|
||||
</fieldset>
|
||||
|
||||
{selectedCharacters.length > 0 && (
|
||||
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
|
||||
{transI18n("import")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{Error && (
|
||||
<div className="text-error text-base mt-2">😭 {Error}</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Selection Info */}
|
||||
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
|
||||
<span className="text-blue-400 font-medium">
|
||||
{transI18n("selectedCharacters")}: {selectedCharacters.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
|
||||
>
|
||||
{transI18n("clearAll")}
|
||||
</button>
|
||||
<button onClick={selectAll} className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
|
||||
{transI18n("selectAll")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
|
||||
</div>
|
||||
)}
|
||||
{/* Character Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
if (newProfile) {
|
||||
newAvatar.profileList.push(newProfile)
|
||||
newAvatar.profileSelect = newAvatar.profileList.length - 1
|
||||
}
|
||||
setAvatar(newAvatar)
|
||||
}
|
||||
|
||||
})
|
||||
setIsOpenImport(false)
|
||||
toast.success(transI18n("importFreeSRDataSuccess"))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-slate-900 p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-white text-2xl font-bold mb-6">{transI18n("freeSRImport")}</h1>
|
||||
|
||||
<div className="flex gap-4 items-center mb-6">
|
||||
<fieldset className="fieldset">
|
||||
<legend className="fieldset-legend">{transI18n("pickAFile")}</legend>
|
||||
<input type="file" accept=".json" className="file-input file-input-info" onChange={handlerReadFile} />
|
||||
<label className="label">{transI18n("onlySupportFreeSRJsonFile")}</label>
|
||||
</fieldset>
|
||||
|
||||
{selectedCharacters.length > 0 && (
|
||||
<button onClick={handleImport} className="btn btn-primary rounded-lg transition-colors">
|
||||
{transI18n("import")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{Error && (
|
||||
<div className="text-error text-base mt-2">😭 {Error}</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* Selection Info */}
|
||||
<div className="mt-6 flex items-center gap-4 bg-slate-800/50 p-4 rounded-lg">
|
||||
<span className="text-blue-400 font-medium">
|
||||
{transI18n("selectedCharacters")}: {selectedCharacters.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="text-error/75 hover:text-error text-sm font-medium px-3 py-1 border border-error/75 rounded hover:bg-error/10 transition-colors cursor-pointer"
|
||||
>
|
||||
{transI18n("clearAll")}
|
||||
</button>
|
||||
<button onClick={selectAll} className="text-primary/75 hover:text-primary text-sm font-medium px-3 py-1 border border-primary/75 rounded hover:bg-primary/10 transition-colors cursor-pointer">
|
||||
{transI18n("selectAll")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{isLoading && (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white"></div>
|
||||
</div>
|
||||
)}
|
||||
{/* Character Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{validFreeSRData && Object.values(validFreeSRData.avatars).filter(it => mapAvatar?.[it.avatar_id]).map((character) => {
|
||||
return (
|
||||
<CharacterInfoCard
|
||||
@@ -219,10 +219,10 @@ export default function FreeSRImport() {
|
||||
selectedCharacters={selectedCharacters}
|
||||
onCharacterToggle={handleCharacterToggle}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user