UPDATE: New cdn, assets
All checks were successful
Gitea Auto Deploy / Deploy-Container (push) Successful in 39s

This commit is contained in:
2026-02-17 23:27:25 +07:00
parent b5713c3138
commit f5541c9527
66 changed files with 56590 additions and 237 deletions

View File

@@ -105,6 +105,7 @@
"level": "等级", "level": "等级",
"relics": "遗器", "relics": "遗器",
"eidolons": "星魂", "eidolons": "星魂",
"lightcones": "光锥" "lightcones": "光锥",
"trailblazer": "开拓者"
} }
} }

View File

@@ -55,15 +55,15 @@
"noCharactersInLineup": "No characters in lineup", "noCharactersInLineup": "No characters in lineup",
"noTurns": "No turns yet", "noTurns": "No turns yet",
"type": "Type", "type": "Type",
"warrior": "Destruction", "warrior": "The Destruction",
"knight": "Preservation", "knight": "The Preservation",
"mage": "Erudition", "mage": "The Erudition",
"priest": "Abundance", "priest": "The Abundance",
"rogue": "The Hunt", "rogue": "The Hunt",
"shaman": "Harmony", "shaman": "The Harmony",
"warlock": "Nihility", "warlock": "The Nihility",
"memory": "Remembrance", "memory": "The Remembrance",
"elation": "Elation", "elation": "The Elation",
"fire": "Fire", "fire": "Fire",
"ice": "Ice", "ice": "Ice",
"imaginary": "Imaginary", "imaginary": "Imaginary",
@@ -105,6 +105,7 @@
"level": "Level", "level": "Level",
"relics": "Relics", "relics": "Relics",
"eidolons": "Eidolons", "eidolons": "Eidolons",
"lightcones": "Lightcones" "lightcones": "Lightcones",
"trailblazer": "Trailblazer"
} }
} }

View File

@@ -105,6 +105,7 @@
"level": "レベル", "level": "レベル",
"relics": "遺物", "relics": "遺物",
"eidolons": "星魂", "eidolons": "星魂",
"lightcones": "光円錐" "lightcones": "光円錐",
"trailblazer": "開拓者"
} }
} }

View File

@@ -105,6 +105,7 @@
"level": "레벨", "level": "레벨",
"relics": "유물", "relics": "유물",
"eidolons": "에이돌론", "eidolons": "에이돌론",
"lightcones": "광추" "lightcones": "광추",
"trailblazer": "개척자"
} }
} }

View File

@@ -105,6 +105,7 @@
"level": "Cấp độ", "level": "Cấp độ",
"relics": "Di vật", "relics": "Di vật",
"eidolons": "Tinh hồn", "eidolons": "Tinh hồn",
"lightcones": "Nón Ánh sáng" "lightcones": "Nón Ánh sáng",
"trailblazer": "Nhà khai phá"
} }
} }

View File

@@ -105,6 +105,7 @@
"level": "等级", "level": "等级",
"relics": "遗器", "relics": "遗器",
"eidolons": "星魂", "eidolons": "星魂",
"lightcones": "光锥" "lightcones": "光锥",
"trailblazer": "开拓者"
} }
} }

View File

@@ -12,24 +12,35 @@ const nextConfig: NextConfig = {
reactStrictMode: false, reactStrictMode: false,
output: 'standalone', output: 'standalone',
images: { images: {
unoptimized: true, unoptimized: true,
remotePatterns: [ remotePatterns: [
{ {
protocol: 'https', protocol: 'https',
hostname: 'localhost', hostname: 'localhost',
pathname: '**', pathname: '**',
}, },
{ {
protocol: 'http', protocol: 'http',
hostname: 'localhost', hostname: 'localhost',
pathname: '**', pathname: '**',
}, },
{ {
protocol: 'https', protocol: "https",
hostname: 'api.hakush.in', hostname: "cdn.kain.id.vn",
pathname: '**', pathname: "**",
}, },
], {
protocol: "http",
hostname: "cdn.kain.id.vn",
pathname: "**",
}
],
},
compiler: {
styledComponents: true,
},
env: {
CDN_URL: "https://cdn.kain.id.vn/firefly/assets/asbres",
}, },
}; };

1133
public/data/character.json Normal file

File diff suppressed because it is too large Load Diff

55232
public/data/monster.json Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/icon/IconJoy.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
public/icon/attack.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
public/icon/crit-rate.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/icon/defence.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
public/icon/effect-res.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/icon/elation.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
public/icon/fire-add.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
public/icon/fire.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

BIN
public/icon/hp.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/icon/ice-add.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
public/icon/ice.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 918 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/icon/imaginary.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
public/icon/knight.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/icon/mage.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
public/icon/memory.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
public/icon/physical.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 B

BIN
public/icon/priest.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
public/icon/quantum.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 B

BIN
public/icon/rogue.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
public/icon/shaman.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/icon/speed.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 984 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
public/icon/thunder.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

BIN
public/icon/warlock.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

BIN
public/icon/warrior.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
public/icon/wind-add.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
public/icon/wind.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 B

View File

@@ -11,10 +11,11 @@ import MultiCharLineChart from "@/components/chart/damageLineForAll";
import DamagePerCycleForAll from "@/components/chart/damagePerCycleForAll"; import DamagePerCycleForAll from "@/components/chart/damagePerCycleForAll";
import DamagePercentChartForAll from "@/components/chart/damagePercentForAll"; import DamagePercentChartForAll from "@/components/chart/damagePercentForAll";
import EnemyBar from "@/components/enemybar"; import EnemyBar from "@/components/enemybar";
import { CharacterBasic, MonsterBasic } from "@/types";
export default function Home() { export default function Home() {
const transI18n = useTranslations("DataAnalysisPage"); const transI18n = useTranslations("DataAnalysisPage");
const { setListAvatar, setListEnemy } = useAvatarDataStore(); const { setListAvatar, setMapAvatar, setListEnemy, setMapEnemy } = useAvatarDataStore();
const { const {
totalAV, totalAV,
totalDamage, totalDamage,
@@ -34,10 +35,20 @@ export default function Home() {
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
const data = await getCharacterListApi(); const avatarData = await getCharacterListApi();
setListAvatar(data); setListAvatar(avatarData);
const avatarMap = avatarData.reduce<Record<string, CharacterBasic>>((acc, m) => {
acc[m.id] = m
return acc
}, {})
setMapAvatar(avatarMap)
const enemyData = await getEnemyListApi(); const enemyData = await getEnemyListApi();
setListEnemy(enemyData); setListEnemy(enemyData);
const monsterMap = enemyData.reduce<Record<string, MonsterBasic>>((acc, m) => {
acc[m.id] = m
return acc
}, {})
setMapEnemy(monsterMap)
}; };
fetchData(); fetchData();
}, [setListAvatar, setListEnemy]); }, [setListAvatar, setListEnemy]);

View File

@@ -2,7 +2,7 @@
import useAvatarDataStore from "@/stores/avatarDataStore"; import useAvatarDataStore from "@/stores/avatarDataStore";
import useBattleDataStore from "@/stores/battleDataStore"; import useBattleDataStore from "@/stores/battleDataStore";
import useLocaleStore from "@/stores/localeStore"; import useLocaleStore from "@/stores/localeStore";
import {attackTypeToString, AvatarHakushiType} from "@/types"; import { attackTypeToString, CharacterBasic } from "@/types";
import { SkillBattleInfo } from "@/types/mics"; import { SkillBattleInfo } from "@/types/mics";
import { useEffect, useState, useRef } from "react"; import { useEffect, useState, useRef } from "react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
@@ -13,7 +13,7 @@ import NameAvatar from "../nameAvatar";
export default function ActionBar() { export default function ActionBar() {
const [selectTurn, setSelectTurn] = useState<SkillBattleInfo | null>(null); const [selectTurn, setSelectTurn] = useState<SkillBattleInfo | null>(null);
const [selectAvatar, setSelectAvatar] = useState<AvatarHakushiType | null>(null); const [selectAvatar, setSelectAvatar] = useState<CharacterBasic | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const { skillHistory, turnHistory, cycleIndex, waveIndex, maxWave } = useBattleDataStore(); const { skillHistory, turnHistory, cycleIndex, waveIndex, maxWave } = useBattleDataStore();
const { listAvatar } = useAvatarDataStore(); const { listAvatar } = useAvatarDataStore();
@@ -29,7 +29,7 @@ export default function ActionBar() {
alignItems: 'center', alignItems: 'center',
}; };
const handleShow = (modalId: string, avatar: AvatarHakushiType, turn: SkillBattleInfo) => { const handleShow = (modalId: string, avatar: CharacterBasic, turn: SkillBattleInfo) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null; const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) { if (modal) {
setSelectAvatar(avatar); setSelectAvatar(avatar);
@@ -105,13 +105,13 @@ export default function ActionBar() {
skillHistory.map((turn, index) => { skillHistory.map((turn, index) => {
const data = listAvatar.find(it => it.id === turn.avatarId.toString()); const data = listAvatar.find(it => it.id === turn.avatarId.toString());
if (!data) return null; if (!data) return null;
const text = getNameChar(locale, data); const text = getNameChar(locale, transI18n, data);
return ( return (
<div key={index} className="h-full md:w-full"> <div key={index} className="h-full md:w-full">
<div <div
onClick={() => handleShow("action_detail_modal", data, turn)} onClick={() => handleShow("action_detail_modal", data, turn)}
className="h-full grid grid-cols-2 gap-2 border bg-base-100 w-full hover:bg-base-200 transition-colors duration-200 border-cyan-400 border-l-4 cursor-pointer min-w-[200px] sm:min-w-[250px] md:min-w-0" className="h-full grid grid-cols-2 gap-2 border bg-base-100 w-full hover:bg-base-200 transition-colors duration-200 border-cyan-400 border-l-4 cursor-pointer min-w-50 sm:min-w-62.5 md:min-w-0"
> >
<div <div
style={contentStyle} style={contentStyle}
@@ -119,21 +119,23 @@ export default function ActionBar() {
> >
<div className="avatar"> <div className="avatar">
<div className="w-12 h-12 rounded-full border-2 flex items-center justify-center bg-base-300 border-cyan-400 border-l-4"> <div className="w-12 h-12 rounded-full border-2 flex items-center justify-center bg-base-300 border-cyan-400 border-l-4">
<Image <Image
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${data.id}.webp`} src={`${process.env.CDN_URL}/${data.icon}`}
unoptimized
crossOrigin="anonymous"
alt={text} alt={text}
width={32} width={125}
height={32} height={125}
className="w-8 h-8 object-contain" className="w-8 h-8 object-contain"
/> />
</div> </div>
</div> </div>
<NameAvatar <NameAvatar
locale={locale} locale={locale}
text={getNameChar(locale, data)} text={getNameChar(locale, transI18n, data)}
className="text-base-content text-center text-sm mt-1 font-medium" className="text-base-content text-center text-sm mt-1 font-medium"
/> />
</div> </div>
<div className="grid grid-cols-1 justify-center gap-2 py-2 w-full"> <div className="grid grid-cols-1 justify-center gap-2 py-2 w-full">
<div className="bg-local text-primary text-xs max-w-full"> <div className="bg-local text-primary text-xs max-w-full">
@@ -195,15 +197,17 @@ export default function ActionBar() {
<span className="font-medium text-base-content/70">{transI18n("character")}:</span> <span className="font-medium text-base-content/70">{transI18n("character")}:</span>
<NameAvatar <NameAvatar
locale={locale} locale={locale}
text={getNameChar(locale, selectAvatar)} text={getNameChar(locale, transI18n, selectAvatar)}
className="font-bold" className="font-bold"
/> />
</div> </div>
</div> </div>
<div className="flex justify-center items-center"> <div className="flex justify-center items-center">
<Image <Image
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${selectAvatar.id}.webp`} src={`${process.env.CDN_URL}/${selectAvatar?.icon}`}
alt={getNameChar(locale, selectAvatar)} unoptimized
crossOrigin="anonymous"
alt={getNameChar(locale, transI18n, selectAvatar)}
width={80} width={80}
height={80} height={80}
className="h-20 w-20 object-cover rounded-full border-2 border-purple-500 shadow-lg shadow-purple-500/20" className="h-20 w-20 object-cover rounded-full border-2 border-purple-500 shadow-lg shadow-purple-500/20"

View File

@@ -2,21 +2,21 @@
import { getNameChar } from '@/helper'; import { getNameChar } from '@/helper';
import useLocaleStore from '@/stores/localeStore'; import useLocaleStore from '@/stores/localeStore';
import { AvatarHakushiType } from '@/types'; import { CharacterBasic } from '@/types';
import NameAvatar from '../nameAvatar'; import NameAvatar from '../nameAvatar';
import useBattleDataStore from '@/stores/battleDataStore'; import useBattleDataStore from '@/stores/battleDataStore';
import Image from 'next/image'; import Image from 'next/image';
import { useTranslations } from 'next-intl';
interface CharacterCardProps { interface CharacterCardProps {
data: AvatarHakushiType data: CharacterBasic
} }
export default function CharacterCard({ data }: CharacterCardProps) { export default function CharacterCard({ data }: CharacterCardProps) {
const { locale } = useLocaleStore(); const { locale } = useLocaleStore();
const text = getNameChar(locale, data)
const { avatarDetail } = useBattleDataStore() const { avatarDetail } = useBattleDataStore()
const transI18n = useTranslations("DataAnalysisPage");
const text = getNameChar(locale, transI18n, data)
return ( return (
<li className="z-10 flex flex-col w-28 items-center p-1 rounded-md shadow-lg bg-linear-to-b from-customStart to-customEnd transform transition-transform duration-300 hover:scale-105 m-1"> <li className="z-10 flex flex-col w-28 items-center p-1 rounded-md shadow-lg bg-linear-to-b from-customStart to-customEnd transform transition-transform duration-300 hover:scale-105 m-1">
@@ -31,21 +31,27 @@ export default function CharacterCard({ data }: CharacterCardProps) {
<Image <Image
width={376} width={376}
height={512} height={512}
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${data.id}.webp`} unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${data.icon}`}
className="w-full h-full rounded-md object-cover" className="w-full h-full rounded-md object-cover"
alt="ALT" alt="ALT"
/> />
<Image <Image
width={48} width={48}
height={48} height={48}
src={`https://api.hakush.in/hsr/UI/element/${data.damageType.toLowerCase()}.webp`} unoptimized
crossOrigin="anonymous"
src={`/icon/${data.damageType.toLowerCase()}.webp`}
className="absolute top-0 left-0 w-6 h-6" className="absolute top-0 left-0 w-6 h-6"
alt={data.damageType.toLowerCase()} alt={data.damageType.toLowerCase()}
/> />
<Image <Image
width={48} width={48}
height={48} height={48}
src={`https://api.hakush.in/hsr/UI/pathicon/${data.baseType.toLowerCase()}.webp`} unoptimized
crossOrigin="anonymous"
src={`/icon/${data.baseType.toLowerCase()}.webp`}
className="absolute top-0 right-0 w-6 h-6" className="absolute top-0 right-0 w-6 h-6"
alt={data.baseType.toLowerCase()} alt={data.baseType.toLowerCase()}
/> />

View File

@@ -24,14 +24,13 @@ export default function MultiCharLineChart() {
const [mode, setMode] = useState<1 | 2>(2); const [mode, setMode] = useState<1 | 2>(2);
const dataByAvatar = useDamageLinesForAll(mode); const dataByAvatar = useDamageLinesForAll(mode);
const avatarIds = Object.keys(dataByAvatar).map(Number); const avatarIds = Object.keys(dataByAvatar).map(Number);
const { listAvatar } = useAvatarDataStore() const { mapAvatar } = useAvatarDataStore()
const { locale } = useLocaleStore(); const { locale } = useLocaleStore();
const transI18n = useTranslations("DataAnalysisPage") const transI18n = useTranslations("DataAnalysisPage")
const data = { const data = {
datasets: avatarIds.map((id, idx) => ({ datasets: avatarIds.map((id, idx) => ({
label: getNameChar(locale, listAvatar.find(it => it.id == id.toString())), label: getNameChar(locale, transI18n, mapAvatar?.[id.toString()]),
data: dataByAvatar[id].map(({ x, y }: { x: number; y: number }) => { data: dataByAvatar[id].map(({ x, y }: { x: number; y: number }) => {
return { return {
x: x.toFixed(2), x: x.toFixed(2),

View File

@@ -48,17 +48,16 @@ const borderPalette = colorPalette.map((c) => c.replace('0.6', '1'));
export default function DamagePerAvatarForAll() { export default function DamagePerAvatarForAll() {
const transI18n = useTranslations("DataAnalysisPage"); const transI18n = useTranslations("DataAnalysisPage");
const { lineup, skillHistory } = useBattleDataStore(); const { lineup, skillHistory } = useBattleDataStore();
const { listAvatar } = useAvatarDataStore(); const { mapAvatar } = useAvatarDataStore();
const { locale } = useLocaleStore(); const { locale } = useLocaleStore();
const [mode, setMode] = useState<number>(2); const [mode, setMode] = useState<number>(2);
const avatarMap = lineup.map((avatar) => { const avatarMap = lineup.map((avatar) => {
const char = listAvatar.find(it => it.id === avatar.avatarId.toString()); if (!mapAvatar?.[avatar.avatarId.toString()]) return undefined;
if (!char) return undefined;
return { return {
avatarId: avatar.avatarId, avatarId: avatar.avatarId,
avatarName: getNameChar(locale, char) avatarName: getNameChar(locale, transI18n, mapAvatar?.[avatar.avatarId.toString()])
}; };
}).filter(Boolean) as { avatarId: number, avatarName: string }[]; }).filter(Boolean) as { avatarId: number, avatarName: string }[];

View File

@@ -27,14 +27,14 @@ export default function DamagePercentChartForAll() {
const [mode, setMode] = useState<1 | 2>(1); const [mode, setMode] = useState<1 | 2>(1);
const damageByAvatar = useDamagePercentPerAvatar(); const damageByAvatar = useDamagePercentPerAvatar();
const damageByType = useDamagePercentByType(); const damageByType = useDamagePercentByType();
const { listAvatar } = useAvatarDataStore(); const { mapAvatar } = useAvatarDataStore();
const { locale } = useLocaleStore(); const { locale } = useLocaleStore();
const transI18n = useTranslations("DataAnalysisPage"); const transI18n = useTranslations("DataAnalysisPage");
const chartData = { const chartData = {
labels: (mode === 1 labels: (mode === 1
? damageByAvatar.map(d => ? damageByAvatar.map(d =>
getNameChar(locale, listAvatar.find(it => it.id == d.avatarId.toString())) getNameChar(locale, transI18n, mapAvatar?.[d.avatarId.toString()])
) )
: damageByType.map(d => transI18n(d.type.toLowerCase())) : damageByType.map(d => transI18n(d.type.toLowerCase()))
), ),

View File

@@ -10,7 +10,7 @@ import NameAvatar from "../nameAvatar";
export default function EnemyBar() { export default function EnemyBar() {
const { enemyDetail } = useBattleDataStore() const { enemyDetail } = useBattleDataStore()
const { listEnemy } = useAvatarDataStore() const { mapEnemy } = useAvatarDataStore()
const { locale } = useLocaleStore() const { locale } = useLocaleStore()
return ( return (
@@ -21,10 +21,12 @@ export default function EnemyBar() {
<div key={uid} className="bg-base-200 rounded-lg p-3 border border-gray-700 w-52 shrink-0"> <div key={uid} className="bg-base-200 rounded-lg p-3 border border-gray-700 w-52 shrink-0">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{listEnemy.find((monster) => monster.child.includes(enemy.id))?.icon?.split("/")?.pop()?.replace(".png", "") && ( {mapEnemy?.[enemy.id.toString()]?.icon && (
<Image <Image
src={`https://api.hakush.in/hsr/UI/monstermiddleicon/${listEnemy.find((monster) => monster.child.includes(enemy.id))?.icon?.split("/")?.pop()?.replace(".png", "")}.webp`} src={`${process.env.CDN_URL}/${mapEnemy?.[enemy.id.toString()]?.icon}`}
alt={enemy.name} alt={enemy.name}
unoptimized
crossOrigin="anonymous"
width={40} width={40}
height={40} height={40}
className="object-cover w-10 h-10 rounded-lg" className="object-cover w-10 h-10 rounded-lg"
@@ -33,7 +35,7 @@ export default function EnemyBar() {
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<NameAvatar <NameAvatar
text={getNameEnemy(locale, listEnemy.find((monster) => monster.child.includes(enemy.id)))} text={getNameEnemy(locale, mapEnemy?.[enemy.id.toString()])}
locale={locale} locale={locale}
className="text-base font-semibold leading-tight truncate overflow-hidden" className="text-base font-semibold leading-tight truncate overflow-hidden"
/> />

View File

@@ -4,7 +4,7 @@ import useBattleDataStore from "@/stores/battleDataStore";
import CharacterCard from "../card/characterCard"; import CharacterCard from "../card/characterCard";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { AvatarHakushiType } from "@/types"; import { CharacterBasic } from "@/types";
import useLocaleStore from "@/stores/localeStore"; import useLocaleStore from "@/stores/localeStore";
import { getNameChar } from '@/helper/getNameChar'; import { getNameChar } from '@/helper/getNameChar';
import SkillBarChart from "../chart/skillBarChart"; import SkillBarChart from "../chart/skillBarChart";
@@ -18,12 +18,12 @@ import NameAvatar from "../nameAvatar";
// import ShowCaseInfo from "../card/showCaseCard"; // import ShowCaseInfo from "../card/showCaseCard";
export default function LineupBar() { export default function LineupBar() {
const [selectedCharacter, setSelectedCharacter] = useState<AvatarHakushiType | undefined>(undefined); const [selectedCharacter, setSelectedCharacter] = useState<CharacterBasic | undefined>(undefined);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const transI18n = useTranslations("DataAnalysisPage"); const transI18n = useTranslations("DataAnalysisPage");
const { lineup, turnHistory, dataAvatar } = useBattleDataStore(); const { lineup, turnHistory, dataAvatar } = useBattleDataStore();
const { listAvatar } = useAvatarDataStore(); const { listAvatar, mapAvatar } = useAvatarDataStore();
const { locale } = useLocaleStore(); const { locale } = useLocaleStore();
const totalDamage = useCalcTotalDmgAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0); const totalDamage = useCalcTotalDmgAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0);
const totalTurn = useCalcTotalTurnAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0) const totalTurn = useCalcTotalTurnAvatar(selectedCharacter ? Number(selectedCharacter.id) : 0)
@@ -33,7 +33,7 @@ export default function LineupBar() {
lineup.some(av => av.avatarId.toString() === item.id) lineup.some(av => av.avatarId.toString() === item.id)
); );
const handleShow = (modalId: string, item: AvatarHakushiType) => { const handleShow = (modalId: string, item: CharacterBasic) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null; const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) { if (modal) {
setSelectedCharacter(item); setSelectedCharacter(item);
@@ -81,16 +81,16 @@ export default function LineupBar() {
<path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M17.982 18.725A7.488 7.488 0 0 0 12 15.75a7.488 7.488 0 0 0-5.982 2.975m11.963 0a9 9 0 1 0-11.963 0m11.963 0A8.966 8.966 0 0 1 12 21a8.966 8.966 0 0 1-5.982-2.275M15 9.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg> </svg>
<span className="text-sm truncate flex flex-row"> <span className="text-sm truncate flex flex-row">
<div> <div>
{transI18n("lastTurn")}: {transI18n("lastTurn")}:
</div> </div>
<NameAvatar <NameAvatar
locale={locale} locale={locale}
text={getNameChar(locale, listAvatar.find(it => it.id === turnHistory.findLast(i => i?.avatarId)?.avatarId?.toString()))} text={getNameChar(locale, transI18n, mapAvatar?.[turnHistory.findLast(i => i?.avatarId)?.avatarId?.toString() || ""])}
/> />
</span> </span>
</div> </div>
</div> </div>
@@ -142,10 +142,10 @@ export default function LineupBar() {
</div> </div>
<div className="border-b border-purple-500/30 px-6 py-4 mb-4"> <div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<NameAvatar <NameAvatar
locale={locale} locale={locale}
text={getNameChar(locale, selectedCharacter).toUpperCase()} text={getNameChar(locale, transI18n, selectedCharacter).toUpperCase()}
className={"font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400"} className={"font-bold text-2xl text-transparent bg-clip-text bg-linear-to-r from-pink-400 to-cyan-400"}
/> />
</div> </div>
@@ -170,8 +170,9 @@ export default function LineupBar() {
<span className="font-bold">{transI18n(selectedCharacter.baseType.toLowerCase())}</span> <span className="font-bold">{transI18n(selectedCharacter.baseType.toLowerCase())}</span>
{selectedCharacter.baseType && ( {selectedCharacter.baseType && (
<Image <Image
unoptimized
src={`https://api.hakush.in/hsr/UI/pathicon/${selectedCharacter.baseType.toLowerCase()}.webp`} crossOrigin="anonymous"
src={`/icon/${selectedCharacter.baseType.toLowerCase()}.webp`}
className="w-6 h-6" className="w-6 h-6"
alt={selectedCharacter.baseType.toLowerCase()} alt={selectedCharacter.baseType.toLowerCase()}
width={24} width={24}
@@ -185,7 +186,9 @@ export default function LineupBar() {
<p className="flex items-center space-x-2"> <p className="flex items-center space-x-2">
<span>{transI18n("element")}:</span> <span>{transI18n("element")}:</span>
<Image <Image
src={`https://api.hakush.in/hsr/UI/element/${selectedCharacter.damageType.toLowerCase()}.webp`} unoptimized
crossOrigin="anonymous"
src={`/icon/${selectedCharacter.damageType.toLowerCase()}.webp`}
className="w-6 h-6" className="w-6 h-6"
alt={selectedCharacter.damageType.toLowerCase()} alt={selectedCharacter.damageType.toLowerCase()}
width={24} width={24}
@@ -212,7 +215,9 @@ export default function LineupBar() {
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<span>{transI18n("lightcones")}:</span> <span>{transI18n("lightcones")}:</span>
<Image <Image
src={`https://api.hakush.in/hsr/UI/lightconemediumicon/${avatar?.Lightcone?.item_id}.webp`} unoptimized
crossOrigin="anonymous"
src={`${process.env.CDN_URL}/spriteoutput/lightconemediumicon/${avatar?.Lightcone?.item_id}.png`}
className="w-12 h-12" className="w-12 h-12"
alt={avatar?.Lightcone?.item_id?.toString() || ""} alt={avatar?.Lightcone?.item_id?.toString() || ""}
width={200} width={200}
@@ -224,8 +229,10 @@ export default function LineupBar() {
<div className="grid grid-cols-3 md:flex md:flex-row w-full"> <div className="grid grid-cols-3 md:flex md:flex-row w-full">
{relicIds.map(it => ( {relicIds.map(it => (
<Image <Image
unoptimized
crossOrigin="anonymous"
key={it} key={it}
src={`https://api.hakush.in/hsr/UI/relicfigures/IconRelic_${it}.webp`} src={`${process.env.CDN_URL}/spriteoutput/relicfigures/IconRelic_${it}.png`}
className="w-12 h-12" className="w-12 h-12"
alt={avatar?.Lightcone?.item_id?.toString() || ""} alt={avatar?.Lightcone?.item_id?.toString() || ""}
width={200} width={200}
@@ -243,8 +250,10 @@ export default function LineupBar() {
</div> </div>
<Image <Image
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${selectedCharacter.id}.webp`} unoptimized
alt={getNameChar(locale, selectedCharacter)} crossOrigin="anonymous"
src={`${process.env.CDN_URL}/${selectedCharacter.icon}`}
alt={getNameChar(locale, transI18n, selectedCharacter)}
className="h-32 w-32 object-cover rounded-full border-2 border-purple-500" className="h-32 w-32 object-cover rounded-full border-2 border-purple-500"
width={128} width={128}
height={128} height={128}

View File

@@ -1,5 +1,6 @@
"use client"; "use client";
import { createContext, PropsWithChildren, useEffect, useState } from "react"; import useLocaleStore from "@/stores/localeStore";
import { createContext, PropsWithChildren, useEffect } from "react";
interface ThemeContextType { interface ThemeContextType {
theme?: string; theme?: string;
@@ -8,7 +9,7 @@ interface ThemeContextType {
export const ThemeContext = createContext<ThemeContextType>({}); export const ThemeContext = createContext<ThemeContextType>({});
export const ThemeProvider = ({ children }: PropsWithChildren) => { export const ThemeProvider = ({ children }: PropsWithChildren) => {
const [theme, setTheme] = useState<string>("light"); const { theme, setTheme } = useLocaleStore()
useEffect(() => { useEffect(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
@@ -19,14 +20,13 @@ export const ThemeProvider = ({ children }: PropsWithChildren) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const changeTheme = (nextTheme: string | null) => { const changeTheme = (nextTheme: string | null) => {
console.log(nextTheme)
if (nextTheme) { if (nextTheme) {
setTheme(nextTheme); setTheme(nextTheme);
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
localStorage.setItem("theme", nextTheme); localStorage.setItem("theme", nextTheme);
} }
} else { } else {
setTheme((prev) => (prev === "light" ? "night" : "light")); setTheme(theme === "winter" ? "night" : "winter");
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
localStorage.setItem("theme", theme); localStorage.setItem("theme", theme);
} }

View File

@@ -1,38 +1,51 @@
import { listCurrentLanguage } from "@/lib/constant"; import { listCurrentLanguage } from "@/lib/constant";
import { AvatarHakushiType, EnemyHakushiType } from "@/types"; import { CharacterBasic, MonsterBasic } from "@/types";
import { useTranslations } from "next-intl";
export function getNameChar(locale: string, data: AvatarHakushiType | undefined): string { type TFunc = ReturnType<typeof useTranslations>
if (!data) {
return ""
}
if (!listCurrentLanguage.hasOwnProperty(locale)) {
return ""
}
let text = data.lang.get(listCurrentLanguage[locale as keyof typeof listCurrentLanguage].toLowerCase()) ?? ""; export function getNameChar(
if (!text) { locale: string,
text = data.lang.get("en") ?? ""; t: TFunc,
} data: CharacterBasic | undefined
if (Number(data.id) % 2 === 0 && Number(data.id) > 8000) { ): string {
text = `Female ${data.damageType} MC` if (!data) return "";
} else if (Number(data.id) > 8000) {
text = `Male ${data.damageType} MC` if (!Object.prototype.hasOwnProperty.call(listCurrentLanguage, locale)) {
} return "";
return text }
const langKey = listCurrentLanguage[locale as keyof typeof listCurrentLanguage].toLowerCase();
let text = data.lang[langKey] ?? "";
if (!text) {
text = data.lang["en"] ?? "";
}
if (Number(data.id) > 8000) {
text = `${t("trailblazer")}${t(data?.baseType?.toLowerCase() ?? "")}`;
}
return text;
} }
export function getNameEnemy(locale: string, data: EnemyHakushiType | undefined): string { export function getNameEnemy(locale: string, data: MonsterBasic | undefined): string {
if (!data) { if (!data) {
return "" return ""
} }
if (!listCurrentLanguage.hasOwnProperty(locale)) { if (!Object.prototype.hasOwnProperty.call(listCurrentLanguage, locale)) {
return "" return ""
} }
let text = data.lang.get(listCurrentLanguage[locale as keyof typeof listCurrentLanguage].toLowerCase()) ?? ""; const langKey = listCurrentLanguage[locale as keyof typeof listCurrentLanguage].toLowerCase();
let text = data.lang[langKey] ?? "";
if (!text) { if (!text) {
text = data.lang.get("en") ?? ""; text = data.lang["en"] ?? "";
} }
return text return text
} }

View File

@@ -1,6 +1,5 @@
import useSocketStore from "@/stores/socketSettingStore"; import useSocketStore from "@/stores/socketSettingStore";
import { EnemyHakushiRawType, EnemyHakushiType } from "@/types"; import { CharacterBasic, MonsterBasic } from "@/types";
import { AvatarHakushiType, AvatarHakushiRawType } from "@/types/avatar";
import axios from 'axios'; import axios from 'axios';
export async function checkConnectTcpApi(): Promise<boolean> { export async function checkConnectTcpApi(): Promise<boolean> {
@@ -23,20 +22,10 @@ export async function checkConnectTcpApi(): Promise<boolean> {
return false return false
} }
export async function getCharacterListApi(): Promise<AvatarHakushiType[]> { export async function getCharacterListApi(): Promise<CharacterBasic[]> {
try { try {
const res = await axios.get<Record<string, AvatarHakushiRawType>>( const res = await axios.get<CharacterBasic[]>("/data/character.json");
'https://api.hakush.in/hsr/data/character.json', return res.data
{
headers: {
'Content-Type': 'application/json',
},
}
);
const data = new Map(Object.entries(res.data));
return Array.from(data.entries()).map(([id, it]) => convertAvatar(id, it));
} catch (error: unknown) { } catch (error: unknown) {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`); console.log(`Error: ${error.response?.status} - ${error.message}`);
@@ -47,20 +36,10 @@ export async function getCharacterListApi(): Promise<AvatarHakushiType[]> {
} }
} }
export async function getEnemyListApi(): Promise<EnemyHakushiType[]> { export async function getEnemyListApi(): Promise<MonsterBasic[]> {
try { try {
const res = await axios.get<Record<string, EnemyHakushiRawType>>( const res = await axios.get<MonsterBasic[]>("/data/monster.json");
'https://api.hakush.in/hsr/data/monster.json', return res.data
{
headers: {
'Content-Type': 'application/json',
},
}
);
const data = new Map(Object.entries(res.data));
return Array.from(data.entries()).map(([id, it]) => convertMonster(id, it));
} catch (error: unknown) { } catch (error: unknown) {
if (axios.isAxiosError(error)) { if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`); console.log(`Error: ${error.response?.status} - ${error.message}`);
@@ -70,46 +49,3 @@ export async function getEnemyListApi(): Promise<EnemyHakushiType[]> {
return []; return [];
} }
} }
function convertAvatar(id: string, item: AvatarHakushiRawType): AvatarHakushiType {
const lang = new Map<string, string>([
['en', item.en],
['kr', item.kr],
['cn', item.cn],
['jp', item.jp]
]);
const result: AvatarHakushiType = {
release: item.release,
icon: item.icon,
rank: item.rank,
baseType: item.baseType,
damageType: item.damageType,
desc: item.desc,
lang: lang,
id: id
};
return result;
}
export function convertMonster(id: string, item: EnemyHakushiRawType): EnemyHakushiType {
const lang = new Map<string, string>([
['en', item.en],
['kr', item.kr],
['cn', item.cn],
['jp', item.jp]
]);
const result: EnemyHakushiType = {
id: id,
rank: item.rank,
camp: item.camp,
icon: item.icon,
child: item.child,
weak: item.weak,
desc: item.desc,
lang: lang
};
return result;
}

View File

@@ -1,19 +1,30 @@
import { AvatarHakushiType, EnemyHakushiType } from '@/types'; import { CharacterBasic, MonsterBasic } from '@/types';
import { create } from 'zustand' import { create } from 'zustand'
interface AvatarDataState { interface AvatarDataState {
listAvatar: AvatarHakushiType[]; listAvatar: CharacterBasic[];
listEnemy: EnemyHakushiType[]; mapAvatar: Record<string, CharacterBasic>;
setListAvatar: (list: AvatarHakushiType[]) => void; listEnemy: MonsterBasic[];
setListEnemy: (list: EnemyHakushiType[]) => void; mapEnemy: Record<string, MonsterBasic>;
setListAvatar: (list: CharacterBasic[]) => void;
setMapAvatar: (data: Record<string, CharacterBasic>) => void;
setListEnemy: (list: MonsterBasic[]) => void;
setMapEnemy: (data: Record<string, MonsterBasic>) => void;
} }
const useAvatarDataStore = create<AvatarDataState>((set) => ({ const useAvatarDataStore = create<AvatarDataState>((set) => ({
listAvatar: [], listAvatar: [],
listEnemy: [], listEnemy: [],
setListAvatar: (list: AvatarHakushiType[]) => set({ listAvatar: list }), mapAvatar: {},
setListEnemy: (list: EnemyHakushiType[]) => set({ listEnemy: list }), mapEnemy: {},
setListAvatar: (list: CharacterBasic[]) => set({ listAvatar: list }),
setMapAvatar: (data: Record<string, CharacterBasic>) => set({ mapAvatar: data }),
setListEnemy: (list: MonsterBasic[]) => set({ listEnemy: list }),
setMapEnemy: (data: Record<string, MonsterBasic>) => set({ mapEnemy: data })
})); }));
export default useAvatarDataStore; export default useAvatarDataStore;

View File

@@ -1,15 +1,25 @@
import { create } from 'zustand' import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware';
interface LocaleState { interface LocaleState {
locale: string; locale: string;
theme: string
setTheme: (newTheme: string) => void;
setLocale: (newLocale: string) => void; setLocale: (newLocale: string) => void;
} }
const useLocaleStore = create<LocaleState>((set) => ({ const useLocaleStore = create<LocaleState>()(persist(
locale: "en", (set) => ({
setLocale: (newLocale: string) => set({ locale: newLocale }), locale: "en",
theme: "night",
})); setTheme: (newTheme: string) => set({ theme: newTheme }),
setLocale: (newLocale: string) => set({ locale: newLocale }),
}),
{
name: 'locale-storage',
storage: createJSONStorage(() => localStorage),
}
));
export default useLocaleStore; export default useLocaleStore;

View File

@@ -1,23 +1,8 @@
export interface AvatarHakushiRawType { export interface CharacterBasic {
release: number;
icon: string;
rank: string;
baseType: string;
damageType: string;
en: string;
desc: string;
kr: string;
cn: string;
jp: string;
}
export interface AvatarHakushiType {
id: string; id: string;
release: number;
icon: string; icon: string;
rank: string; rank: string;
baseType: string; baseType: string;
damageType: string; damageType: string;
desc: string; lang: Record<string, string>;
lang: Map<string, string>;
} }

View File

@@ -11,26 +11,12 @@ export interface InitializeEnemyType {
enemy: EnemyType enemy: EnemyType
} }
export interface EnemyHakushiRawType { export interface MonsterBasic {
rank: string;
camp: string | null;
icon: string;
child: number[];
weak: string[];
en: string;
desc: string;
kr: string;
cn: string;
jp: string;
}
export interface EnemyHakushiType {
id: string; id: string;
rank: string; rank: string;
camp: string | null;
icon: string; icon: string;
child: number[]; image: string;
weak: string[]; weak: string[];
desc: string; desc: Record<string, string>;
lang: Map<string, string>; lang: Record<string, string>;
} }