UPDATE: add more ui for showcase card, fix some bug
All checks were successful
Gitea Auto Deploy / Deploy-Container (push) Successful in 2m1s

This commit is contained in:
2025-10-05 17:01:07 +07:00
parent b8d7c1ab78
commit b76a4a4e9c
11 changed files with 301 additions and 146 deletions

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import Image from "next/image" import Image from "next/image"
import { useEffect, useState } from "react" import { useEffect } from "react"
import CharacterCard from "../card/characterCard" import CharacterCard from "../card/characterCard"
import useLocaleStore from "@/stores/localeStore" import useLocaleStore from "@/stores/localeStore"
import useAvatarStore from "@/stores/avatarStore" import useAvatarStore from "@/stores/avatarStore"
@@ -8,9 +8,17 @@ import { useTranslations } from "next-intl"
export default function AvatarBar({ onClose }: { onClose?: () => void }) { 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 {
const [listPath, setListPath] = useState<Record<string, boolean>>({ "knight": false, "mage": false, "priest": false, "rogue": false, "shaman": false, "warlock": false, "warrior": false, "memory": false }) listAvatar,
const { listAvatar, setAvatarSelected, setSkillSelected, setFilter, filter } = useAvatarStore() setAvatarSelected,
setSkillSelected,
setFilter,
filter,
listElement,
listPath,
setListElement,
setListPath
} = useAvatarStore()
const transI18n = useTranslations("DataPage") const transI18n = useTranslations("DataPage")
const { locale } = useLocaleStore() const { locale } = useLocaleStore()
@@ -40,7 +48,7 @@ export default function AvatarBar({ onClose }: { onClose?: () => void }) {
<div <div
key={index} key={index}
onClick={() => { onClick={() => {
setListElement((prev) => ({ ...prev, [key]: !prev[key] })) setListElement({ ...listElement, [key]: !listElement[key] })
}} }}
className="hover:bg-gray-600 grid items-center justify-items-center cursor-pointer rounded-md shadow-lg" className="hover:bg-gray-600 grid items-center justify-items-center cursor-pointer rounded-md shadow-lg"
style={{ style={{
@@ -60,7 +68,7 @@ export default function AvatarBar({ onClose }: { onClose?: () => void }) {
<div <div
key={index} key={index}
onClick={() => { onClick={() => {
setListPath((prev) => ({ ...prev, [key]: !prev[key] })) setListPath({ ...listPath, [key]: !listPath[key] })
}} }}
className="hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-lg cursor-pointer" className="hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-lg cursor-pointer"
style={{ style={{

View File

@@ -22,12 +22,16 @@ export default function CopyImport() {
setSelectedProfiles, setSelectedProfiles,
filterCopy, filterCopy,
setFilterCopy, setFilterCopy,
setAvatarCopySelected setAvatarCopySelected,
listElement,
listPath,
listRank,
setListElement,
setListPath,
setListRank
} = useCopyProfileStore() } = useCopyProfileStore()
const transI18n = useTranslations("DataPage") 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({ const [message, setMessage] = useState({
type: "", type: "",
text: "" text: ""
@@ -49,8 +53,9 @@ export default function CopyImport() {
element: Object.keys(listElement).filter((key) => listElement[key]), element: Object.keys(listElement).filter((key) => listElement[key]),
rarity: Object.keys(listRank).filter((key) => listRank[key]) rarity: Object.keys(listRank).filter((key) => listRank[key])
}) })
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [listPath, listRank, locale]) }, [listPath, listRank, listElement, locale, setFilterCopy])
const clearSelection = () => { const clearSelection = () => {
setSelectedProfiles([]); setSelectedProfiles([]);
@@ -142,7 +147,7 @@ export default function CopyImport() {
<div <div
key={index} key={index}
onClick={() => { onClick={() => {
setListPath((prev) => ({ ...prev, [key]: !prev[key] })) setListPath({ ...listPath, [key]: !listPath[key] })
}} }}
className="w-[50px] h-[50px] hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer" className="w-[50px] h-[50px] hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
style={{ style={{
@@ -167,7 +172,7 @@ export default function CopyImport() {
<div <div
key={index} key={index}
onClick={() => { onClick={() => {
setListElement((prev) => ({ ...prev, [key]: !prev[key] })) setListElement({ ...listElement, [key]: !listElement[key] })
}} }}
className="w-[50px] h-[50px] hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer" className="w-[50px] h-[50px] hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
style={{ style={{
@@ -192,7 +197,7 @@ export default function CopyImport() {
<div <div
key={index} key={index}
onClick={() => { onClick={() => {
setListRank((prev) => ({ ...prev, [key]: !prev[key] })) setListRank({ ...listRank, [key]: !listRank[key] })
}} }}
className="w-[50px] h-[50px] hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer" className="w-[50px] h-[50px] hover:bg-gray-600 grid items-center justify-items-center rounded-md shadow-md cursor-pointer"
style={{ style={{
@@ -207,9 +212,10 @@ export default function CopyImport() {
</div> </div>
<div className="grid grid-cols-1 gap-2"> <div className="grid grid-cols-1 gap-2">
{listCopyAvatar.length > 0 && (
<div> <div>
<div>{transI18n("characterName")}</div> <div>{transI18n("characterName")}</div>
{listCopyAvatar.length > 0 && (
<SelectCustomImage <SelectCustomImage
customSet={listCopyAvatar.map((avatar) => ({ customSet={listCopyAvatar.map((avatar) => ({
value: avatar.id.toString(), value: avatar.id.toString(),
@@ -221,8 +227,9 @@ export default function CopyImport() {
placeholder="Character Select" placeholder="Character Select"
setSelectedCustomSet={(value) => setAvatarCopySelected(listCopyAvatar.find((avatar) => avatar.id.toString() === value) || null)} setSelectedCustomSet={(value) => setAvatarCopySelected(listCopyAvatar.find((avatar) => avatar.id.toString() === value) || null)}
/> />
)}
</div> </div>
)}
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
"use client" "use client"
import { useEffect, useState } from "react" import { useEffect } from "react"
import Image from "next/image"; import Image from "next/image";
import useLocaleStore from "@/stores/localeStore" import useLocaleStore from "@/stores/localeStore"
import useLightconeStore from "@/stores/lightconeStore"; import useLightconeStore from "@/stores/lightconeStore";
@@ -11,10 +11,17 @@ import useModelStore from "@/stores/modelStore";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
export default function LightconeBar() { 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 { locale } = useLocaleStore()
const { listLightcone, filter, setFilter, defaultFilter } = useLightconeStore() const {
listLightcone,
filter,
setFilter,
defaultFilter,
listPath,
listRank,
setListPath,
setListRank
} = useLightconeStore()
const { setAvatar, avatars } = useUserDataStore() const { setAvatar, avatars } = useUserDataStore()
const { avatarSelected } = useAvatarStore() const { avatarSelected } = useAvatarStore()
const { setIsOpenLightcone } = useModelStore() const { setIsOpenLightcone } = useModelStore()
@@ -35,7 +42,7 @@ export default function LightconeBar() {
} }
setListPath(newListPath) setListPath(newListPath)
setListRank(newListRank) setListRank(newListRank)
}, [defaultFilter]) }, [defaultFilter, setListPath, setListRank])
useEffect(() => { useEffect(() => {
setFilter({ setFilter({
@@ -72,7 +79,7 @@ export default function LightconeBar() {
<div <div
key={index} key={index}
onClick={() => { onClick={() => {
setListPath((prev) => ({ ...prev, [key]: !prev[key] })) setListPath({ ...listPath, [key]: !listPath[key] })
}} }}
className="h-[38px] w-[38px] md:h-[50px] md:w-[50px] hover:bg-gray-600 grid place-items-center rounded-md shadow-lg cursor-pointer" className="h-[38px] w-[38px] md:h-[50px] md:w-[50px] hover:bg-gray-600 grid place-items-center rounded-md shadow-lg cursor-pointer"
style={{ style={{
@@ -96,7 +103,7 @@ export default function LightconeBar() {
<div <div
key={index} key={index}
onClick={() => { onClick={() => {
setListRank((prev) => ({ ...prev, [key]: !prev[key] })) setListRank({ ...listRank, [key]: !listRank[key] })
}} }}
className="h-[38px] w-[38px] md:h-[50px] md:w-[50px] hover:bg-gray-600 grid place-items-center rounded-md shadow-lg cursor-pointer" className="h-[38px] w-[38px] md:h-[50px] md:w-[50px] hover:bg-gray-600 grid place-items-center rounded-md shadow-lg cursor-pointer"
style={{ style={{

View File

@@ -9,6 +9,7 @@ import { useMemo } from "react";
import useAvatarStore from "@/stores/avatarStore"; import useAvatarStore from "@/stores/avatarStore";
import { calcAffixBonus, calcBaseStatRaw, calcBonusStatRaw, calcMainAffixBonus, calcMainAffixBonusRaw, calcPromotion, calcSubAffixBonusRaw, replaceByParam } from "@/helper"; import { calcAffixBonus, calcBaseStatRaw, calcBonusStatRaw, calcMainAffixBonus, calcMainAffixBonusRaw, calcPromotion, calcSubAffixBonusRaw, replaceByParam } from "@/helper";
import { mappingStats } from "@/constant/constant"; import { mappingStats } from "@/constant/constant";
import RelicShowcase from "../showcaseCard/relicShowcase";
export default function QuickView() { export default function QuickView() {
const { avatarSelected, mapAvatarInfo } = useAvatarStore() const { avatarSelected, mapAvatarInfo } = useAvatarStore()
const { mapLightconeInfo } = useLightconeStore() const { mapLightconeInfo } = useLightconeStore()
@@ -80,7 +81,9 @@ export default function QuickView() {
return { return {
property: subAffixMap?.[subValue?.sub_affix_id]?.property, property: subAffixMap?.[subValue?.sub_affix_id]?.property,
valueAffix: calcAffixBonus(subAffixMap?.[subValue?.sub_affix_id], subValue?.step, subValue?.count), valueAffix: calcAffixBonus(subAffixMap?.[subValue?.sub_affix_id], subValue?.step, subValue?.count),
detail: mappingStats?.[subAffixMap?.[subValue?.sub_affix_id]?.property] detail: mappingStats?.[subAffixMap?.[subValue?.sub_affix_id]?.property],
step: subValue?.step,
count: subValue?.count
} }
}) })
} }
@@ -398,7 +401,7 @@ export default function QuickView() {
<div key={index} className="flex flex-row items-center justify-between"> <div key={index} className="flex flex-row items-center justify-between">
<div className="flex flex-row items-center"> <div className="flex flex-row items-center">
<NextImage src={stat?.icon || ""} alt="Stat Icon" width={40} height={40} className="h-auto w-10 p-1 mx-1 bg-black/20 rounded-full" /> <NextImage src={stat?.icon || ""} alt="Stat Icon" width={40} height={40} className="h-auto w-10 p-1 mx-1 bg-black/20 rounded-full" />
<span className="font-bold">{stat.name}</span> <div className="font-bold">{stat.name}</div>
</div> </div>
<div className="ml-3 mr-3 flex-grow border rounded opacity-50" /> <div className="ml-3 mr-3 flex-grow border rounded opacity-50" />
<div className="flex cursor-default flex-col text-right font-bold">{ <div className="flex cursor-default flex-col text-right font-bold">{
@@ -442,60 +445,76 @@ export default function QuickView() {
{relicStats?.map((relic, index) => { {relicStats?.map((relic, index) => {
if (!relic) return null if (!relic) return null
return ( return (
<div key={index} className="black-blur relative flex flex-row items-center rounded-lg border-2 p-1 border-yellow-600"> <div
<div className="flex"> key={index}
<NextImage src={relic?.img || ""} width={80} height={80} alt="Relic Icon" className="h-auto w-20" /> className="relative w-full flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600/60 bg-gradient-to-r from-yellow-600/20 to-transparent"
>
{/* Subtle glow overlay */}
<div className="absolute inset-0 rounded-s-lg pointer-events-none"></div>
<div className="flex relative">
<div className="absolute inset-0 rounded-lg blur-lg -z-10"></div>
<NextImage
src={relic?.img || ""}
width={78}
height={78}
alt="Relic Icon"
className="h-auto w-[78px] rounded-lg"
/>
<div <div
className="absolute text-yellow-500 font-bold z-10" className="absolute text-yellow-400 font-bold z-10 drop-shadow-[0_0_6px_rgba(251,191,36,0.8)]"
style={{ style={{
left: '0.7rem', left: '0.65rem',
bottom: '-0.5rem', bottom: '-0.45rem',
fontSize: '1.1rem', fontSize: '1.05rem',
letterSpacing: '-0.1em', 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> </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 bg-black/20 rounded-full" /> <div className=" flex w-1/6 flex-col items-center justify-center">
<span className="text-base text-[#f1a23c]">{relic?.mainAffix?.valueAffix + relic?.mainAffix?.detail?.unit}</span> <div className="relative">
<span className="black-blur rounded px-1 text-xs">+{relic?.mainAffix?.level}</span> <div className="absolute inset-0 bg-yellow-500/15 rounded-full blur-md -z-10"></div>
<NextImage
src={relic?.mainAffix?.detail?.icon || ""}
width={35}
height={35}
alt="Main Affix Icon"
className="h-auto w-[35px]"
/>
</div> </div>
<div style={{ opacity: 0.5, height: '80px', borderLeftWidth: '1px' }}></div> <span className="text-base text-yellow-400 font-semibold drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]">
<div className="m-auto grid w-1/2 grid-cols-2 gap-2"> {relic?.mainAffix?.valueAffix + relic?.mainAffix?.detail?.unit}
</span>
<span className="bg-black/20 backdrop-blur-sm rounded px-1.5 py-0.5 text-xs border border-white/10">
+{relic?.mainAffix?.level}
</span>
</div>
<div style={{ opacity: 0.3, height: '78px', borderLeftWidth: '1px' }}></div>
<div className="grid w-[65%] m-2 grid-cols-2 gap-1">
{relic?.subAffix?.map((subAffix, index) => { {relic?.subAffix?.map((subAffix, index) => {
if (!subAffix) return null if (!subAffix) return null
return ( return (
<div key={index} className="flex flex-col"> <RelicShowcase key={index} relic={relic} />
<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 bg-black/20 rounded-full" /> </div>
) : ( </div>
<div className="h-9 w-9 bg-black/20 rounded flex items-center justify-center"> )
<span className="text-xs text-white">?</span> })}
{(!relicStats || !relicStats?.length) && (
<div className="flex flex-col items-center justify-center">
<div className="text-center p-6 rounded-lg bg-black/40 backdrop-blur-sm border border-white/10">
<span className="text-lg text-gray-400">{transI18n("noRelicEquipped")}</span>
</div>
</div> </div>
)} )}
<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> </div>
) )

View File

@@ -16,6 +16,8 @@ import { useTranslations } from 'next-intl';
import useAffixStore from '@/stores/affixStore'; import useAffixStore from '@/stores/affixStore';
import useRelicStore from '@/stores/relicStore'; import useRelicStore from '@/stores/relicStore';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import RelicShowcase from './relicShowcase';
export default function ShowCaseInfo() { export default function ShowCaseInfo() {
const { avatarSelected, mapAvatarInfo } = useAvatarStore() const { avatarSelected, mapAvatarInfo } = useAvatarStore()
@@ -162,7 +164,9 @@ export default function ShowCaseInfo() {
return { return {
property: subAffixMap?.[subValue?.sub_affix_id]?.property, property: subAffixMap?.[subValue?.sub_affix_id]?.property,
valueAffix: calcAffixBonus(subAffixMap?.[subValue?.sub_affix_id], subValue?.step, subValue?.count), valueAffix: calcAffixBonus(subAffixMap?.[subValue?.sub_affix_id], subValue?.step, subValue?.count),
detail: mappingStats?.[subAffixMap?.[subValue?.sub_affix_id]?.property] detail: mappingStats?.[subAffixMap?.[subValue?.sub_affix_id]?.property],
step: subValue?.step,
count: subValue?.count
} }
}) })
} }
@@ -495,7 +499,7 @@ export default function ShowCaseInfo() {
}, [avatarSelected]) }, [avatarSelected])
return ( return (
<div className="flex flex-col justify-start m-2 text-white"> <div className="flex flex-col justify-start m-1 text-white">
<div className="flex items-center justify-start mt-4 mb-4"> <div className="flex items-center justify-start mt-4 mb-4">
<button className="btn btn-success w-24 text-sm" onClick={handleSaveImage}>Save Img</button> <button className="btn btn-success w-24 text-sm" onClick={handleSaveImage}>Save Img</button>
</div> </div>
@@ -503,7 +507,7 @@ export default function ShowCaseInfo() {
<div className="overflow-auto"> <div className="overflow-auto">
<div <div
ref={cardRef} ref={cardRef}
className=" relative min-h-[650px] w-[1400px] rounded-3xl transition-all duration-500" className=" relative min-h-[650px] w-[1600px] rounded-3xl transition-all duration-500 overflow-hidden"
style={{ style={{
backgroundColor: `${applyBrightness(avgColor, 0.3)}`, backgroundColor: `${applyBrightness(avgColor, 0.3)}`,
backdropFilter: "blur(50px)", backdropFilter: "blur(50px)",
@@ -522,7 +526,7 @@ export default function ShowCaseInfo() {
<NextImage <NextImage
ref={imgRef} ref={imgRef}
src={`https://api.hakush.in/hsr/UI/avatardrawcard/${avatarSelected?.id}.webp`} src={`https://api.hakush.in/hsr/UI/avatardrawcard/${avatarSelected?.id}.webp`}
className="object-cover scale-[2]" className="object-cover scale-[2] overflow-hidden"
alt="Character Preview" alt="Character Preview"
width={1024} width={1024}
height={1024} height={1024}
@@ -607,7 +611,6 @@ export default function ShowCaseInfo() {
{avatarData && avatarInfo && avatarSkillTree && traceShowCaseMap[avatarSelected?.baseType || ""] {avatarData && avatarInfo && avatarSkillTree && traceShowCaseMap[avatarSelected?.baseType || ""]
&& Object.values(traceShowCaseMap[avatarSelected?.baseType || ""] || []).map((item, index) => { && Object.values(traceShowCaseMap[avatarSelected?.baseType || ""] || []).map((item, index) => {
return ( return (
<div key={`row-${index}`} className="flex flex-row items-center"> <div key={`row-${index}`} className="flex flex-row items-center">
{item.map((btn, idx) => { {item.map((btn, idx) => {
@@ -803,7 +806,6 @@ export default function ShowCaseInfo() {
</div> </div>
</div> </div>
{/* Chỉ số */}
<div className="flex justify-center items-center flex-col gap-1 mt-1 "> <div className="flex justify-center items-center flex-col gap-1 mt-1 ">
<div className="flex gap-1 text-sm "> <div className="flex gap-1 text-sm ">
<div className="flex items-center gap-1 rounded bg-black/30 px-1 w-fit py-1"> <div className="flex items-center gap-1 rounded bg-black/30 px-1 w-fit py-1">
@@ -872,7 +874,7 @@ export default function ShowCaseInfo() {
}} }}
/> />
<div> <div>
<span className="black-blur bg-black/50 flex w-5 justify-center rounded px-1.5 py-0.5">{setEffect.count}</span> <span className="black-blur bg-black/30 flex w-5 justify-center rounded px-1.5 py-0.5">{setEffect.count}</span>
</div> </div>
</div> </div>
) )
@@ -881,65 +883,22 @@ export default function ShowCaseInfo() {
</div> </div>
<div className="w-1/3 z-10"> <div className="w-1/3 z-10">
<div className="flex h-[650px] flex-col justify-between py-3 text-lg"> <div className="flex h-[650px] flex-col justify-between py-3 mr-1 text-lg w-full" >
{relicStats?.map((relic, index) => { {relicStats?.map((relic, index) => {
if (!relic) return null if (!relic) return null
return ( return (
<div key={index} className="black-blur relative flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600"> <RelicShowcase key={index} relic={relic} />
<div className="flex"> )
<NextImage src={relic?.img || ""} width={80} height={80} alt="Relic Icon" className="h-auto w-20" /> })}
<div {(!relicStats || !relicStats?.length) && (
className="absolute text-yellow-500 font-bold z-10" <div className="flex flex-col items-center justify-center">
style={{ <div className="text-center p-6">
left: '0.7rem', <span className="text-lg text-white">{transI18n("noRelicEquipped")}</span>
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> </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> </div>

View File

@@ -0,0 +1,99 @@
"use client"
import NextImage from "next/image"
import { RelicShowcaseType } from "@/types";
export default function RelicShowcase({
relic,
}: {
relic: RelicShowcaseType;
}) {
return (
<>
<div
className="relative w-full flex flex-row items-center rounded-s-lg border-l-2 p-1 border-yellow-600/60 bg-gradient-to-r from-yellow-600/20 to-transparent"
>
{/* Subtle glow overlay */}
<div className="absolute inset-0 rounded-s-lg pointer-events-none"></div>
<div className="flex relative">
<div className="absolute inset-0 rounded-lg blur-lg -z-10"></div>
<NextImage
src={relic?.img || ""}
width={78}
height={78}
alt="Relic Icon"
className="h-auto w-[78px] rounded-lg"
/>
<div
className="absolute text-yellow-400 font-bold z-10 drop-shadow-[0_0_6px_rgba(251,191,36,0.8)]"
style={{
left: '0.65rem',
bottom: '-0.45rem',
fontSize: '1.05rem',
letterSpacing: '-0.1em',
}}
>
</div>
</div>
<div className=" flex w-1/6 flex-col items-center justify-center">
<div className="relative">
<div className="absolute inset-0 bg-yellow-500/15 rounded-full blur-md -z-10"></div>
<NextImage
src={relic?.mainAffix?.detail?.icon || ""}
width={35}
height={35}
alt="Main Affix Icon"
className="h-auto w-[35px]"
/>
</div>
<span className="text-base text-yellow-400 font-semibold drop-shadow-[0_0_4px_rgba(251,191,36,0.5)]">
{relic?.mainAffix?.valueAffix + relic?.mainAffix?.detail?.unit}
</span>
<span className="bg-black/20 backdrop-blur-sm rounded px-1.5 py-0.5 text-xs border border-white/10">
+{relic?.mainAffix?.level}
</span>
</div>
<div style={{ opacity: 0.3, height: '78px', borderLeftWidth: '1px' }}></div>
<div className="grid w-[65%] m-2 grid-cols-2 gap-1">
{relic?.subAffix?.map((subAffix, index) => {
if (!subAffix) return null
return (
<div key={index} className="flex flex-col">
<div className="relative flex flex-row items-center bg-black/20 backdrop-blur-sm rounded-md p-1 border border-white/5 min-w-0">
{subAffix?.detail?.icon ? (
<NextImage
src={subAffix?.detail?.icon || ""}
width={32}
height={32}
alt="Sub Affix Icon"
className="h-auto w-6 flex-shrink-0"
/>
) : (
<div className="h-6 w-6 bg-black/60 rounded flex items-center justify-center border border-white/10 flex-shrink-0">
<span className="text-xs text-white/50">?</span>
</div>
)}
<span className="text-xs text-gray-200 ml-0.5 truncate flex-1 min-w-0">
+{subAffix?.valueAffix + subAffix?.detail?.unit}
</span>
{subAffix.step > 1 && (
<span className="ml-1 bg-yellow-600/20 text-yellow-400 rounded-full px-1 py-0.5 text-[10px] font-semibold border border-yellow-600/30 flex-shrink-0 leading-none">
{subAffix?.step}
</span>
)}
</div>
</div>
)
})}
</div>
</div>
</>
)
}

View File

@@ -9,6 +9,10 @@ interface AvatarState {
avatarSelected: CharacterBasic | null; avatarSelected: CharacterBasic | null;
mapAvatarInfo: Record<string, CharacterDetail>; mapAvatarInfo: Record<string, CharacterDetail>;
skillSelected: string | null; skillSelected: string | null;
listElement: Record<string, boolean>;
listPath: Record<string, boolean>;
setListElement: (newListElement: Record<string, boolean>) => void;
setListPath: (newListPath: Record<string, boolean>) => void;
setListAvatar: (newListAvatar: CharacterBasic[]) => void; setListAvatar: (newListAvatar: CharacterBasic[]) => void;
setAvatarSelected: (newAvatarSelected: CharacterBasic) => void; setAvatarSelected: (newAvatarSelected: CharacterBasic) => void;
setFilter: (newFilter: FilterAvatarType) => void; setFilter: (newFilter: FilterAvatarType) => void;
@@ -30,6 +34,10 @@ const useAvatarStore = create<AvatarState>((set, get) => ({
avatarSelected: null, avatarSelected: null,
skillSelected: null, skillSelected: null,
mapAvatarInfo: {}, mapAvatarInfo: {},
listElement: { "fire": false, "ice": false, "imaginary": false, "physical": false, "quantum": false, "thunder": false, "wind": false },
listPath: { "knight": false, "mage": false, "priest": false, "rogue": false, "shaman": false, "warlock": false, "warrior": false, "memory": false },
setListElement: (newListElement: Record<string, boolean>) => set({ listElement: newListElement }),
setListPath: (newListPath: Record<string, boolean>) => set({ listPath: newListPath }),
setSkillSelected: (newSkillSelected: string | null) => set({ skillSelected: newSkillSelected }), setSkillSelected: (newSkillSelected: string | null) => set({ skillSelected: newSkillSelected }),
setListAvatar: (newListAvatar: CharacterBasic[]) => set({ listAvatar: newListAvatar, listRawAvatar: newListAvatar }), setListAvatar: (newListAvatar: CharacterBasic[]) => set({ listAvatar: newListAvatar, listRawAvatar: newListAvatar }),
setAvatarSelected: (newAvatarSelected: CharacterBasic) => set({ avatarSelected: newAvatarSelected }), setAvatarSelected: (newAvatarSelected: CharacterBasic) => set({ avatarSelected: newAvatarSelected }),

View File

@@ -7,6 +7,12 @@ interface CopyProfileState {
listRawCopyAvatar: CharacterBasic[]; listRawCopyAvatar: CharacterBasic[];
filterCopy: FilterAvatarType; filterCopy: FilterAvatarType;
avatarCopySelected: CharacterBasic | null; avatarCopySelected: CharacterBasic | null;
listElement: Record<string, boolean>;
listPath: Record<string, boolean>;
listRank: Record<string, boolean>;
setListElement: (newListElement: Record<string, boolean>) => void;
setListPath: (newListPath: Record<string, boolean>) => void;
setListRank: (newListRank: Record<string, boolean>) => void;
setSelectedProfiles: (newListAvatar: AvatarProfileCardType[]) => void; setSelectedProfiles: (newListAvatar: AvatarProfileCardType[]) => void;
setAvatarCopySelected: (newAvatarSelected: CharacterBasic | null) => void; setAvatarCopySelected: (newAvatarSelected: CharacterBasic | null) => void;
setFilterCopy: (newFilter: FilterAvatarType) => void; setFilterCopy: (newFilter: FilterAvatarType) => void;
@@ -25,6 +31,12 @@ const useCopyProfileStore = create<CopyProfileState>((set, get) => ({
locale: "", locale: "",
}, },
avatarCopySelected: null, avatarCopySelected: null,
listElement: { "fire": false, "ice": false, "imaginary": false, "physical": false, "quantum": false, "thunder": false, "wind": false },
listPath: { "knight": false, "mage": false, "priest": false, "rogue": false, "shaman": false, "warlock": false, "warrior": false, "memory": false },
listRank: { "4": false, "5": false },
setListElement: (newListElement: Record<string, boolean>) => set({ listElement: newListElement }),
setListPath: (newListPath: Record<string, boolean>) => set({ listPath: newListPath }),
setListRank: (newListRank: Record<string, boolean>) => set({ listRank: newListRank }),
setListCopyAvatar: (newListAvatar: CharacterBasic[]) => set({ listCopyAvatar: newListAvatar, listRawCopyAvatar: newListAvatar }), setListCopyAvatar: (newListAvatar: CharacterBasic[]) => set({ listCopyAvatar: newListAvatar, listRawCopyAvatar: newListAvatar }),
setAvatarCopySelected: (newAvatarSelected: CharacterBasic | null) => set({ avatarCopySelected: newAvatarSelected }), setAvatarCopySelected: (newAvatarSelected: CharacterBasic | null) => set({ avatarCopySelected: newAvatarSelected }),
setFilterCopy: (newFilter: FilterAvatarType) => { setFilterCopy: (newFilter: FilterAvatarType) => {

View File

@@ -4,9 +4,13 @@ import { create } from 'zustand'
interface LightconeState { interface LightconeState {
listLightcone: LightConeBasic[]; listLightcone: LightConeBasic[];
listRawLightcone: LightConeBasic[]; listRawLightcone: LightConeBasic[];
listPath: Record<string, boolean>;
listRank: Record<string, boolean>;
filter: FilterLightconeType; filter: FilterLightconeType;
defaultFilter: { path: string[], rarity: string[] }; defaultFilter: { path: string[], rarity: string[] };
mapLightconeInfo: Record<string, LightConeDetail>; mapLightconeInfo: Record<string, LightConeDetail>;
setListPath: (newListPath: Record<string, boolean>) => void;
setListRank: (newListRank: Record<string, boolean>) => void;
setDefaultFilter: (newDefaultFilter: { path: string[], rarity: string[] }) => void; setDefaultFilter: (newDefaultFilter: { path: string[], rarity: string[] }) => void;
setListLightcone: (newListLightcone: LightConeBasic[]) => void; setListLightcone: (newListLightcone: LightConeBasic[]) => void;
setFilter: (newFilter: FilterLightconeType) => void; setFilter: (newFilter: FilterLightconeType) => void;
@@ -18,6 +22,7 @@ const useLightconeStore = create<LightconeState>((set, get) => ({
listLightcone: [], listLightcone: [],
listRawLightcone: [], listRawLightcone: [],
mapLightconeInfo: {}, mapLightconeInfo: {},
filter: { filter: {
name: "", name: "",
path: [], path: [],
@@ -25,6 +30,10 @@ const useLightconeStore = create<LightconeState>((set, get) => ({
rarity: [], rarity: [],
}, },
defaultFilter: { path: [], rarity: [] }, defaultFilter: { path: [], rarity: [] },
listPath: { "knight": false, "mage": false, "priest": false, "rogue": false, "shaman": false, "warlock": false, "warrior": false, "memory": false },
listRank: { "3": false, "4": false, "5": false },
setListPath: (newListPath: Record<string, boolean>) => set({ listPath: newListPath }),
setListRank: (newListRank: Record<string, boolean>) => set({ listRank: newListRank }),
setDefaultFilter: (newDefaultFilter: { path: string[], rarity: string[] }) => set({ defaultFilter: newDefaultFilter }), setDefaultFilter: (newDefaultFilter: { path: string[], rarity: string[] }) => set({ defaultFilter: newDefaultFilter }),
setListLightcone: (newListLightcone: LightConeBasic[]) => set({ listLightcone: newListLightcone, listRawLightcone: newListLightcone }), setListLightcone: (newListLightcone: LightConeBasic[]) => set({ listLightcone: newListLightcone, listRawLightcone: newListLightcone }),
setFilter: (newFilter: FilterLightconeType) => { setFilter: (newFilter: FilterLightconeType) => {

View File

@@ -20,3 +20,4 @@ export * from "./monsterValue"
export * from "./peakDetail" export * from "./peakDetail"
export * from "./monsterDetail" export * from "./monsterDetail"
export * from "./extraData" export * from "./extraData"
export * from "./showcase"

26
src/types/showcase.tsx Normal file
View File

@@ -0,0 +1,26 @@
export type RelicShowcaseType = {
img: string;
mainAffix: {
property: string;
level: number;
valueAffix: string;
detail: {
name: string;
icon: string;
unit: string;
baseStat: string;
};
};
subAffix: {
property: string;
valueAffix: string;
detail: {
name: string;
icon: string;
unit: string;
baseStat: string;
};
step: number;
count: number;
}[]
}