UPDATE: responsive ui, optimaze ux
All checks were successful
Gitea Auto Deploy / Deploy-Container (push) Successful in 1m42s

This commit is contained in:
2025-09-27 17:14:07 +07:00
parent f1a2cddbea
commit a6f4659ec9
21 changed files with 833 additions and 136 deletions

View File

@@ -17,6 +17,7 @@ import useGlobalStore from "@/stores/globalStore";
import { connectToPS, syncDataToPS } from "@/helper";
import CopyImport from "../importBar/copy";
import useCopyProfileStore from "@/stores/copyProfile";
import AvatarBar from "../avatarBar";
export default function ActionBar() {
@@ -25,7 +26,14 @@ export default function ActionBar() {
const { setListCopyAvatar } = useCopyProfileStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore()
const { isOpenCreateProfile, setIsOpenCreateProfile, isOpenCopy, setIsOpenCopy } = useModelStore()
const {
isOpenCreateProfile,
setIsOpenCreateProfile,
isOpenCopy,
setIsOpenCopy,
isOpenAvatars,
setIsOpenAvatars
} = useModelStore()
const { avatars, setAvatar } = useUserDataStore()
const [profileName, setProfileName] = useState("");
const [formState, setFormState] = useState("EDIT");
@@ -85,6 +93,7 @@ export default function ActionBar() {
// Handle ESC key to close modal
useEffect(() => {
if (!isOpenCreateProfile) {
handleCloseModal("update_profile_modal");
return;
@@ -93,6 +102,12 @@ export default function ActionBar() {
handleCloseModal("copy_profile_modal");
return;
}
console.log(isOpenAvatars)
if (!isOpenAvatars) {
handleCloseModal("avatars_modal");
return;
}
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpenCreateProfile) {
@@ -101,12 +116,15 @@ export default function ActionBar() {
if (event.key === 'Escape' && isOpenCopy) {
handleCloseModal("copy_profile_modal");
}
if (event.key === 'Escape' && isOpenAvatars) {
handleCloseModal("avatars_modal");
}
};
window.addEventListener('keydown', handleEscKey);
return () => window.removeEventListener('keydown', handleEscKey);
}, [isOpenCopy, isOpenCreateProfile]);
}, [isOpenCopy, isOpenCreateProfile, isOpenAvatars]);
const actionMove = (path: string) => {
router.push(`/${path}`)
@@ -154,49 +172,48 @@ export default function ActionBar() {
}
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">
<div className="w-full px-4 pb-4 bg-base-200">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 items-center justify-items-center">
<div className="flex flex-col justify-center w-full">
<div className="flex flex-wrap items-center gap-2 ">
<div className="flex flex-wrap items-center h-full opacity-80 lg:hover:opacity-100 cursor-pointer text-base md:text-lg lg:text-xl">
{avatarSelected && (
<>
<div className="flex items-center justify-start h-full w-full">
<Image
src={ `/icon/${avatarSelected.damageType.toLowerCase()}.webp`}
src={`/icon/${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>
</>
<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">
<div className="flex flex-wrap items-center gap-2">
<span className="text-base opacity-70 font-bold w-16">{transI18n("profile")}:</span>
<div className="dropdown dropdown-end w-full">
<div className="dropdown dropdown-center md:dropdown-start">
<div
tabIndex={0}
role="button"
className="btn btn-warning border-info btn-soft gap-1 min-w-0"
className="btn btn-warning border-info btn-soft gap-1"
>
<span className="truncate max-w-24 font-bold">
<span className="truncate max-w-48 font-bold">
{profileCurrent?.profile_name}
</span>
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -204,7 +221,7 @@ export default function ActionBar() {
</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">
<ul className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box min-w-max w-full mt-1 border border-base-300 max-h-60 overflow-y-auto">
{listProfile.map((profile, index) => (
<li key={index} className="grid grid-cols-12">
<button
@@ -269,7 +286,7 @@ export default function ActionBar() {
<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("")
@@ -284,23 +301,82 @@ export default function ActionBar() {
</li>
</ul>
</div>
<div className=" grid grid-cols-2 w-full sm:hidden gap-2">
<button
onClick={() => {
setIsOpenAvatars(true)
handleShow("avatars_modal")
}}
className="col-span-1 btn btn-warning btn-sm w-full">
{transI18n("avatars")}
</button>
<div className="col-span-1 dropdown dropdown-center w-full">
<label tabIndex={0} className="btn btn-info btn-sm w-full">
{transI18n("actions")}
</label>
<ul
tabIndex={0}
className="dropdown-content z-10 menu p-2 shadow bg-base-100 rounded-box min-w-max w-full mt-1 border border-base-300 max-h-60 overflow-y-auto"
>
<li>
<button onClick={() => actionMove('')}>
{transI18n("characterInformation")}
</button>
</li>
<li>
<button onClick={() => actionMove('relics-info')}>
{transI18n("relics")}
</button>
</li>
<li>
<button onClick={() => actionMove('eidolons-info')}>
{transI18n("eidolons")}
</button>
</li>
<li>
<button onClick={() => actionMove('skills-info')}>
{transI18n("skills")}
</button>
</li>
<li>
<button onClick={() => actionMove('showcase-card')}>
{transI18n("showcaseCard")}
</button>
</li>
<li>
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
<div className="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 className="btn btn-success btn-sm" onClick={() => actionMove('skills-info')}>{transI18n("skills")}</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove('showcase-card')}>{transI18n("showcaseCard")}</button>
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm"> {isConnectPS ? transI18n("sync") : transI18n("connectPs")}</button>
</div>
<div className="hidden sm:grid grid-cols-3 gap-2 w-full">
<button className="btn btn-success btn-sm" onClick={() => actionMove("")}>
{transI18n("characterInformation")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("relics-info")}>
{transI18n("relics")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("eidolons-info")}>
{transI18n("eidolons")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("skills-info")}>
{transI18n("skills")}
</button>
<button className="btn btn-success btn-sm" onClick={() => actionMove("showcase-card")}>
{transI18n("showcaseCard")}
</button>
<button onClick={handleConnectOrSyncPS} className="btn btn-primary btn-sm">
{isConnectPS ? transI18n("sync") : transI18n("connectPs")}
</button>
</div>
<dialog id="update_profile_modal" className="modal sm:modal-middle backdrop-blur-sm">
<dialog id="update_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
@@ -335,7 +411,7 @@ export default function ActionBar() {
<div className="modal-action">
<button className="btn btn-success btn-sm sm:btn-md" onClick={handleUpdateProfile}>
{formState === "CREATE" ? transI18n("create") : transI18n("update")}
{formState === "CREATE" ? transI18n("create") : transI18n("update")}
</button>
</div>
</div>
@@ -366,6 +442,32 @@ export default function ActionBar() {
</div>
</dialog>
<dialog id="avatars_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={() => {
setIsOpenAvatars(false)
handleCloseModal("avatars_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("avatars").toUpperCase()}
</h3>
</div>
<AvatarBar onClose={() => {setIsOpenAvatars(false); handleCloseModal("avatars_modal")}} />
</div>
</dialog>
</div>
</div>
);

View File

@@ -7,13 +7,12 @@ import useAvatarStore from "@/stores/avatarStore"
import { useTranslations } from "next-intl"
export default function AvatarBar() {
export default function AvatarBar({ onClose }: { onClose?: () => void }) {
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, setAvatarSelected, setSkillSelected, setFilter, filter } = useAvatarStore()
const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore()
useEffect(() => {
@@ -31,12 +30,12 @@ export default function AvatarBar() {
<div className="flex justify-center">
<input type="text"
placeholder={transI18n("placeholderCharacter")}
className="input input-bordered input-primary w-full max-w-xs"
className="input input-bordered input-primary w-full"
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">
<div className="grid 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.keys(listElement).map((key, index) => (
<div
key={index}
@@ -56,7 +55,7 @@ export default function AvatarBar() {
))}
</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]">
<div className="grid 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.keys(listPath).map((key, index) => (
<div
key={index}
@@ -81,9 +80,13 @@ export default function AvatarBar() {
</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">
<ul className="grid 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); setSkillSelected(null)}}>
<div key={index} onClick={() => {
setAvatarSelected(item);
setSkillSelected(null)
if (onClose) onClose()
}}>
<CharacterCard data={item} />
</div>
))}

View File

@@ -7,10 +7,13 @@ import ParseText from "../parseText";
import useLocaleStore from "@/stores/localeStore";
import useUserDataStore from "@/stores/userDataStore";
import { useMemo } from "react";
import { useTranslations } from "next-intl";
export default function EidolonsInfo() {
const { avatarSelected, mapAvatarInfo } = useListAvatarStore()
const { locale } = useLocaleStore()
const transI18n = useTranslations("DataPage")
const { setAvatars, avatars } = useUserDataStore()
const charRank = useMemo(() => {
@@ -23,6 +26,11 @@ export default function EidolonsInfo() {
}, [avatarSelected, avatars, locale, mapAvatarInfo]);
return (
<div className="bg-base-100 rounded-xl p-6 shadow-lg">
<h2 className="flex items-center gap-2 text-2xl font-bold mb-6 text-base-content">
<div className="w-2 h-6 bg-gradient-to-b from-primary to-primary/50 rounded-full"></div>
{transI18n("eidolons")}
</h2>
<div className="grid grid-cols-1 m-4 p-4 font-bold gap-4 w-fit max-h-[77vh] min-h-[50vh] overflow-y-scroll overflow-x-hidden">
{charRank && avatars[avatarSelected?.id || ""] && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@@ -63,5 +71,6 @@ export default function EidolonsInfo() {
</div>
)}
</div>
</div>
);
}

View File

@@ -134,7 +134,7 @@ export default function CopyImport() {
<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">
<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">

View File

@@ -160,7 +160,12 @@ export default function AsBar() {
excludeSet={[]}
selectedCustomSet={as_config.event_id.toString()}
placeholder={transI18n("selectASEvent")}
setSelectedCustomSet={(id) => setAsConfig({ ...as_config, event_id: Number(id), challenge_id: 0, buff_id: 0 })}
setSelectedCustomSet={(id) => setAsConfig({
...as_config,
event_id: Number(id),
challenge_id: mapASInfo[Number(id)]?.Level.slice(-1)[0]?.Id || 0,
buff_id: 0
})}
/>
</div>
{/* Settings */}

View File

@@ -127,7 +127,11 @@ export default function MocBar() {
excludeSet={[]}
selectedCustomSet={moc_config.event_id.toString()}
placeholder={transI18n("selectMOCEvent")}
setSelectedCustomSet={(id) => setMocConfig({ ...moc_config, event_id: Number(id), challenge_id: 0 })}
setSelectedCustomSet={(id) => setMocConfig({
...moc_config,
event_id: Number(id),
challenge_id: mapMOCInfo[Number(id)]?.slice(-1)[0]?.Id || 0,
})}
/>
</div>
{/* Settings */}
@@ -140,7 +144,10 @@ export default function MocBar() {
<select
value={moc_config.challenge_id}
className="select select-success"
onChange={(e) => setMocConfig({ ...moc_config, challenge_id: Number(e.target.value) })}
onChange={(e) => setMocConfig({
...moc_config,
challenge_id: Number(e.target.value)
})}
>
<option value={0} disabled={true}>Select a Floor</option>
{mapMOCInfo[moc_config.event_id.toString()]?.map((moc) => (

View File

@@ -134,7 +134,11 @@ export default function PfBar() {
excludeSet={[]}
selectedCustomSet={pf_config.event_id.toString()}
placeholder={transI18n("selectPFEvent")}
setSelectedCustomSet={(id) => setPfConfig({ ...pf_config, event_id: Number(id), challenge_id: 0, buff_id: 0 })}
setSelectedCustomSet={(id) => setPfConfig({
...pf_config,
event_id: Number(id),
challenge_id: mapPFInfo[Number(id)]?.Level.slice(-1)[0]?.Id || 0,
buff_id: 0 })}
/>
</div>
{/* Settings */}

View File

@@ -0,0 +1,504 @@
"use client"
import NextImage from "next/image"
import useLightconeStore from "@/stores/lightconeStore";
import useAffixStore from "@/stores/affixStore";
import useUserDataStore from "@/stores/userDataStore";
import useRelicStore from "@/stores/relicStore";
import { useTranslations } from "next-intl";
import { useMemo } from "react";
import useAvatarStore from "@/stores/avatarStore";
import { calcAffixBonus, calcBaseStatRaw, calcBonusStatRaw, calcMainAffixBonus, calcMainAffixBonusRaw, calcPromotion, calcSubAffixBonusRaw, replaceByParam } from "@/helper";
import { mappingStats } from "@/constant/constant";
export default function QuickView() {
const { avatarSelected, mapAvatarInfo } = useAvatarStore()
const { mapLightconeInfo } = useLightconeStore()
const { mapMainAffix, mapSubAffix } = useAffixStore()
const { avatars } = useUserDataStore()
const transI18n = useTranslations("DataPage")
const { mapRelicInfo } = useRelicStore()
const avatarInfo = useMemo(() => {
if (!avatarSelected) return
return mapAvatarInfo[avatarSelected.id]
}, [avatarSelected, mapAvatarInfo])
const avatarSkillTree = useMemo(() => {
if (!avatarSelected || !avatars[avatarSelected.id]) return {}
if (avatars[avatarSelected.id].enhanced) {
return avatarInfo?.Enhanced[avatars[avatarSelected.id].enhanced.toString()].SkillTrees || {}
}
return avatarInfo?.SkillTrees || {}
}, [avatarSelected, avatarInfo, avatars])
const avatarData = useMemo(() => {
if (!avatarSelected) return
return avatars[avatarSelected.id]
}, [avatarSelected, avatars])
const avatarProfile = useMemo(() => {
if (!avatarSelected || !avatarData) return
return avatarData?.profileList?.[avatarData?.profileSelect]
}, [avatarSelected, avatarData])
const relicEffects = useMemo(() => {
const avatar = avatars[avatarSelected?.id || ""];
const relicCount: { [key: string]: number } = {};
if (avatar) {
for (const relic of Object.values(avatar.profileList[avatar.profileSelect].relics)) {
if (relicCount[relic.relic_set_id]) {
relicCount[relic.relic_set_id]++;
} else {
relicCount[relic.relic_set_id] = 1;
}
}
}
const listEffects: { key: string, count: number }[] = [];
Object.entries(relicCount).forEach(([key, value]) => {
if (value >= 2) {
listEffects.push({ key: key, count: value });
}
});
return listEffects;
}, [avatars, avatarSelected]);
const relicStats = useMemo(() => {
if (!avatarSelected || !avatarProfile?.relics || !mapMainAffix || !mapSubAffix) return
return Object.entries(avatarProfile?.relics).map(([key, value]) => {
const mainAffixMap = mapMainAffix["5" + key]
const subAffixMap = mapSubAffix["5"]
if (!mainAffixMap || !subAffixMap) return
return {
img: `https://api.hakush.in/hsr/UI/relicfigures/IconRelic_${value.relic_set_id}_${key}.webp`,
mainAffix: {
property: mainAffixMap?.[value?.main_affix_id]?.property,
level: value?.level,
valueAffix: calcMainAffixBonus(mainAffixMap?.[value?.main_affix_id], value?.level),
detail: mappingStats?.[mainAffixMap?.[value?.main_affix_id]?.property]
},
subAffix: value?.sub_affixes?.map((subValue) => {
return {
property: subAffixMap?.[subValue?.sub_affix_id]?.property,
valueAffix: calcAffixBonus(subAffixMap?.[subValue?.sub_affix_id], subValue?.step, subValue?.count),
detail: mappingStats?.[subAffixMap?.[subValue?.sub_affix_id]?.property]
}
})
}
})
}, [avatarSelected, avatarProfile, mapMainAffix, mapSubAffix])
const characterStats = useMemo(() => {
if (!avatarSelected || !avatarData) return
const charPromotion = calcPromotion(avatarData.level)
const statsData: Record<string, {
value: number,
base: number,
name: string,
icon: string,
unit: string,
round: number
}> = {
HP: {
value: calcBaseStatRaw(
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.HPBase,
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.HPAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.HPBase,
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.HPAdd,
avatarData.level
),
name: "HP",
icon: "/icon/hp.webp",
unit: "",
round: 0
},
ATK: {
value: calcBaseStatRaw(
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.AttackBase,
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.AttackAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.AttackBase,
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.AttackAdd,
avatarData.level
),
name: "ATK",
icon: "/icon/attack.webp",
unit: "",
round: 0
},
DEF: {
value: calcBaseStatRaw(
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.DefenceBase,
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.DefenceAdd,
avatarData.level
),
base: calcBaseStatRaw(
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.DefenceBase,
mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.DefenceAdd,
avatarData.level
),
name: "DEF",
icon: "/icon/defence.webp",
unit: "",
round: 0
},
SPD: {
value: mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.SpeedBase || 0,
base: mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.SpeedBase || 0,
name: "SPD",
icon: "/icon/speed.webp",
unit: "",
round: 1
},
CRITRate: {
value: mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.CriticalChance || 0,
base: mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.CriticalChance || 0,
name: "CRIT Rate",
icon: "/icon/crit-rate.webp",
unit: "%",
round: 1
},
CRITDmg: {
value: mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.CriticalDamage || 0,
base: mapAvatarInfo?.[avatarSelected.id]?.Stats[charPromotion]?.CriticalDamage || 0,
name: "CRIT DMG",
icon: "/icon/crit-damage.webp",
unit: "%",
round: 1
},
BreakEffect: {
value: 0,
base: 0,
name: "Break Effect",
icon: "/icon/break-effect.webp",
unit: "%",
round: 1
},
EffectRES: {
value: 0,
base: 0,
name: "Effect RES",
icon: "/icon/effect-res.webp",
unit: "%",
round: 1
},
EnergyRate: {
value: 0,
base: 0,
name: "Energy Rate",
icon: "/icon/energy-rate.webp",
unit: "%",
round: 1
},
EffectHitRate: {
value: 0,
base: 0,
name: "Effect Hit Rate",
icon: "/icon/effect-hit-rate.webp",
unit: "%",
round: 1
},
HealBoost: {
value: 0,
base: 0,
name: "Healing Boost",
icon: "/icon/healing-boost.webp",
unit: "%",
round: 1
},
PhysicalAdd: {
value: 0,
base: 0,
name: "Physical Boost",
icon: "/icon/physical-add.webp",
unit: "%",
round: 1
},
FireAdd: {
value: 0,
base: 0,
name: "Fire Boost",
icon: "/icon/fire-add.webp",
unit: "%",
round: 1
},
IceAdd: {
value: 0,
base: 0,
name: "Ice Boost",
icon: "/icon/ice-add.webp",
unit: "%",
round: 1
},
ThunderAdd: {
value: 0,
base: 0,
name: "Thunder Boost",
icon: "/icon/thunder-add.webp",
unit: "%",
round: 1
},
WindAdd: {
value: 0,
base: 0,
name: "Wind Boost",
icon: "/icon/wind-add.webp",
unit: "%",
round: 1
},
QuantumAdd: {
value: 0,
base: 0,
name: "Quantum Boost",
icon: "/icon/quantum-add.webp",
unit: "%",
round: 1
},
ImaginaryAdd: {
value: 0,
base: 0,
name: "Imaginary Boost",
icon: "/icon/imaginary-add.webp",
unit: "%",
round: 1
},
}
if (avatarProfile?.lightcone && mapLightconeInfo[avatarProfile?.lightcone?.item_id]) {
const lightconePromotion = calcPromotion(avatarProfile?.lightcone?.level)
statsData.HP.value += calcBaseStatRaw(
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHP,
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd,
avatarProfile?.lightcone?.level
)
statsData.HP.base += calcBaseStatRaw(
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHP,
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseHPAdd,
avatarProfile?.lightcone?.level
)
statsData.ATK.value += calcBaseStatRaw(
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseAttack,
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseAttackAdd,
avatarProfile?.lightcone?.level
)
statsData.ATK.base += calcBaseStatRaw(
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseAttack,
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseAttackAdd,
avatarProfile?.lightcone?.level
)
statsData.DEF.value += calcBaseStatRaw(
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseDefence,
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseDefenceAdd,
avatarProfile?.lightcone?.level
)
statsData.DEF.base += calcBaseStatRaw(
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseDefence,
mapLightconeInfo?.[avatarProfile?.lightcone?.item_id]?.Stats[lightconePromotion]?.BaseDefenceAdd,
avatarProfile?.lightcone?.level
)
const bonusData = mapLightconeInfo[avatarProfile?.lightcone?.item_id].Bonus?.[avatarProfile?.lightcone.rank - 1]
if (bonusData && bonusData.length > 0) {
const bonusSpd = bonusData.filter((bonus) => bonus.type === "BaseSpeed")
const bonusOther = bonusData.filter((bonus) => bonus.type !== "BaseSpeed")
bonusSpd.forEach((bonus) => {
statsData.SPD.value += bonus.value
statsData.SPD.base += bonus.value
})
bonusOther.forEach((bonus) => {
const statsBase = mappingStats?.[bonus.type]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(bonus.type, statsData[statsBase].base, bonus.value)
}
})
}
}
if (avatarSkillTree) {
Object.values(avatarSkillTree).forEach((value) => {
if (value?.["1"]
&& value?.["1"]?.PointID
&& typeof avatarData?.data?.skills?.[value?.["1"]?.PointID] === "number"
&& avatarData?.data?.skills?.[value?.["1"]?.PointID] !== 0
&& value?.["1"]?.StatusAddList
&& value?.["1"].StatusAddList.length > 0) {
value?.["1"]?.StatusAddList.forEach((status) => {
const statsBase = mappingStats?.[status?.PropertyType]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(status?.PropertyType, statsData[statsBase].base, status.Value)
}
})
}
})
}
if (avatarProfile?.relics && mapMainAffix && mapSubAffix) {
Object.entries(avatarProfile?.relics).forEach(([key, value]) => {
const mainAffixMap = mapMainAffix["5" + key]
const subAffixMap = mapSubAffix["5"]
if (!mainAffixMap || !subAffixMap) return
const mainStats = mappingStats?.[mainAffixMap?.[value.main_affix_id]?.property]?.baseStat
if (mainStats && statsData[mainStats]) {
statsData[mainStats].value += calcMainAffixBonusRaw(mainAffixMap?.[value.main_affix_id], value.level, statsData[mainStats].base)
}
value?.sub_affixes.forEach((subValue) => {
const subStats = mappingStats?.[subAffixMap?.[subValue.sub_affix_id]?.property]?.baseStat
if (subStats && statsData[subStats]) {
statsData[subStats].value += calcSubAffixBonusRaw(subAffixMap?.[subValue.sub_affix_id], subValue.step, subValue.count, statsData[subStats].base)
}
})
})
}
if (relicEffects && relicEffects.length > 0) {
relicEffects.forEach((relic) => {
const dataBonus = mapRelicInfo?.[relic.key]?.Bonus
if (!dataBonus || Object.keys(dataBonus).length === 0) return
Object.entries(dataBonus || {}).forEach(([key, value]) => {
if (relic.count < Number(key)) return
value.forEach((bonus) => {
const statsBase = mappingStats?.[bonus.type]?.baseStat
if (statsBase && statsData[statsBase]) {
statsData[statsBase].value += calcBonusStatRaw(bonus.type, statsData[statsBase].base, bonus.value)
}
})
})
})
}
return statsData
}, [
avatarSelected,
avatarData,
mapAvatarInfo,
avatarProfile?.lightcone,
avatarProfile?.relics,
mapLightconeInfo,
mapMainAffix,
mapSubAffix,
relicEffects,
mapRelicInfo,
avatarSkillTree
])
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="col-span-1 md:col-span-2 flex flex-col justify-between py-3 z-10">
<div className="flex w-full flex-col justify-between gap-y-0.5 text-base h-[500px]">
{Object.entries(characterStats || {})?.map(([key, stat], index) => {
if (!stat || (key.includes("Add") && stat.value === 0)) return null
return (
<div key={index} className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center">
<NextImage src={stat?.icon || ""} alt="Stat Icon" width={40} height={40} className="h-auto w-10 p-2" />
<span className="font-bold">{stat.name}</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">{
stat.value ? stat.unit === "%" ? (stat.value * 100).toFixed(stat.round) : stat.value.toFixed(stat.round) : 0
}{stat.unit}</div>
</div>
)
})}
<hr />
</div>
<div className="flex flex-col items-center gap-1 w-full my-2">
{relicEffects.map((setEffect, index) => {
const relicInfo = mapRelicInfo[setEffect.key];
if (!relicInfo) return null;
return (
<div key={index} className="flex w-full flex-row justify-between text-left">
<div
className="font-bold truncate max-w-full mr-1"
style={{
fontSize: 'clamp(0.5rem, 2vw, 1rem)'
}}
dangerouslySetInnerHTML={{
__html: replaceByParam(
relicInfo.Name,
[]
)
}}
/>
<div>
<span className="black-blur bg-black/50 flex w-5 justify-center rounded px-1.5 py-0.5">{setEffect.count}</span>
</div>
</div>
)
})}
</div>
</div>
<div className="z-10">
<div className="grid grid-cols-1 gap-2 justify-between py-3 text-lg">
{relicStats?.map((relic, index) => {
if (!relic) return null
return (
<div key={index} className="black-blur relative flex flex-row items-center rounded-lg border-2 p-1 border-yellow-600">
<div className="flex">
<NextImage src={relic?.img || ""} width={80} height={80} alt="Relic Icon" className="h-auto w-20" />
<div
className="absolute text-yellow-500 font-bold z-10"
style={{
left: '0.7rem',
bottom: '-0.5rem',
fontSize: '1.1rem',
letterSpacing: '-0.1em',
textShadow: `
0 0 0.2em #f59e0b,
0 0 0.4em #f59e0b,
0 0 0.8em #f59e0b,
-0.05em -0.05em 0.05em rgba(0,0,0,0.7),
0.05em 0.05em 0.05em rgba(0,0,0,0.7)
`
}}
>
</div>
</div>
<div className="mx-1 flex w-1/6 flex-col items-center justify-center">
<NextImage src={relic?.mainAffix?.detail?.icon || ""} width={36} height={36} alt="Main Affix Icon" className="h-auto w-9" />
<span className="text-base text-[#f1a23c]">{relic?.mainAffix?.valueAffix + relic?.mainAffix?.detail?.unit}</span>
<span className="black-blur rounded px-1 text-xs">+{relic?.mainAffix?.level}</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">
{relic?.subAffix?.map((subAffix, index) => {
if (!subAffix) return null
return (
<div key={index} className="flex flex-col">
<div className="flex flex-row items-center">
{subAffix?.detail?.icon ? (
<NextImage src={subAffix?.detail?.icon || ""} width={36} height={36} alt="Sub Affix Icon" className="h-auto w-9" />
) : (
<div className="h-9 w-9 bg-black/50 rounded flex items-center justify-center">
<span className="text-xs text-white">?</span>
</div>
)}
<span className="text-sm">+{subAffix?.valueAffix + subAffix?.detail?.unit}</span>
</div>
</div>
)
})}
</div>
</div>
)
})}
{(!relicStats || !relicStats?.length) && <div className="flex flex-col items-center justify-center ">
<span className="text-lg">{transI18n("noRelicEquipped")}</span>
</div>}
</div>
</div>
</div>
)
}

View File

@@ -11,6 +11,7 @@ import useModelStore from '@/stores/modelStore';
import { replaceByParam } from '@/helper';
import useRelicMakerStore from '@/stores/relicMakerStore';
import useAffixStore from '@/stores/affixStore';
import QuickView from "../quickView";
export default function RelicsInfo() {
const { avatars, setAvatars } = useUserDataStore()
@@ -27,7 +28,12 @@ export default function RelicsInfo() {
listSelectedSubStats,
} = useRelicMakerStore()
const { mapSubAffix } = useAffixStore()
const { isOpenRelic, setIsOpenRelic } = useModelStore()
const {
isOpenRelic,
setIsOpenRelic,
isOpenQuickView,
setIsOpenQuickView
} = useModelStore()
const transI18n = useTranslations("DataPage")
const { mapRelicInfo } = useRelicStore()
@@ -35,14 +41,12 @@ export default function RelicsInfo() {
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();
@@ -55,11 +59,18 @@ export default function RelicsInfo() {
handleCloseModal("action_detail_modal");
return;
};
if (!isOpenQuickView) {
handleCloseModal("quick_view_modal");
return;
};
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape' && isOpenRelic) {
handleCloseModal("action_detail_modal");
}
if (event.key === 'Escape' && isOpenQuickView) {
handleCloseModal("quick_view_modal");
}
};
window.addEventListener('keydown', handleEscKey);
@@ -114,7 +125,7 @@ export default function RelicsInfo() {
setListSelectedSubStats(newSubAffixes)
}
setIsOpenRelic(true)
handleShow("action_detail_modal")
}
@@ -153,7 +164,7 @@ export default function RelicsInfo() {
{transI18n("relics")}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-2xl mx-auto">
<div className="grid grid-cols-2 md: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
@@ -229,6 +240,15 @@ export default function RelicsInfo() {
{transI18n("deleteRelic")}
</button>
</div>
<button
onClick={() => {
setIsOpenQuickView(true)
handleShow("quick_view_modal")
}}
className="btn btn-info w-full mt-2"
>
{transI18n("quickView")}
</button>
</div>
</div>
@@ -302,7 +322,10 @@ export default function RelicsInfo() {
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")}
onClick={() => {
setIsOpenRelic(false)
handleCloseModal("action_detail_modal")
}}
>
</motion.button>
@@ -311,6 +334,30 @@ export default function RelicsInfo() {
</div>
</dialog>
<dialog id="quick_view_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={() => {
setIsOpenQuickView(false)
handleCloseModal("quick_view_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("quickView").toUpperCase()}
</h3>
</div>
<QuickView />
</div>
</dialog>
</div>
);
}

View File

@@ -1,7 +1,7 @@
"use client";
import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react';
import useListAvatarStore from "@/stores/avatarStore";
import useAvatarStore from "@/stores/avatarStore";
import { FastAverageColor, FastAverageColorResult } from 'fast-average-color';
import NextImage from 'next/image';
import ParseText from '../parseText';
@@ -18,7 +18,7 @@ import useRelicStore from '@/stores/relicStore';
import { toast } from 'react-toastify';
export default function ShowCaseInfo() {
const { avatarSelected, mapAvatarInfo } = useListAvatarStore()
const { avatarSelected, mapAvatarInfo } = useAvatarStore()
const { mapLightconeInfo } = useLightconeStore()
const { mapMainAffix, mapSubAffix } = useAffixStore()
const { avatars } = useUserDataStore()
@@ -469,6 +469,7 @@ export default function ShowCaseInfo() {
mapRelicInfo,
avatarSkillTree
])
const applyBrightness = useCallback((hex: string, brightness: number): string => {
const r = Math.round(parseInt(hex.slice(1, 3), 16) * brightness);
const g = Math.round(parseInt(hex.slice(3, 5), 16) * brightness);

View File

@@ -179,9 +179,9 @@ export default function SkillsInfo() {
cursor-pointer transition-all duration-200 ease-in-out
shadow-[0_0_5px_white] flex justify-center items-center
hover:scale-110
${btn.size === "small" ? "w-[2vw] h-[2vw] bg-white" : ""}
${btn.size === "medium" ? "w-[3vw] h-[3vw] bg-white" : ""}
${btn.size === "big" ? "w-[3.5vw] h-[3.5vw] bg-black" : ""}
${btn.size === "small" ? "w-[6vw] h-[6vw] md:w-[2vw] md:h-[2vw] bg-white" : ""}
${btn.size === "medium" ? "w-[8vw] h-[8vw] md:w-[3vw] md:h-[3vw] bg-white" : ""}
${btn.size === "big" ? "w-[9vw] h-[9vw] md:w-[3.5vw] md:h-[3.5vw] bg-black" : ""}
${skillSelected === btn.id ? "border-4 border-primary" : ""}
${avatarData?.data.skills?.[avatarSkillTree?.[btn.id]?.["1"]?.PointID] === 0 ? "opacity-50 cursor-not-allowed" : ""}
`}