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": "等级",
"relics": "遗器",
"eidolons": "星魂",
"lightcones": "光锥"
"lightcones": "光锥",
"trailblazer": "开拓者"
}
}

View File

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

View File

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

View File

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

View File

@@ -105,6 +105,7 @@
"level": "Cấp độ",
"relics": "Di vật",
"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": "等级",
"relics": "遗器",
"eidolons": "星魂",
"lightcones": "光锥"
"lightcones": "光锥",
"trailblazer": "开拓者"
}
}

View File

@@ -25,12 +25,23 @@ const nextConfig: NextConfig = {
pathname: '**',
},
{
protocol: 'https',
hostname: 'api.hakush.in',
pathname: '**',
protocol: "https",
hostname: "cdn.kain.id.vn",
pathname: "**",
},
{
protocol: "http",
hostname: "cdn.kain.id.vn",
pathname: "**",
}
],
},
compiler: {
styledComponents: true,
},
env: {
CDN_URL: "https://cdn.kain.id.vn/firefly/assets/asbres",
},
};
export default withBundleAnalyzer(withNextIntl(nextConfig));

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

View File

@@ -2,7 +2,7 @@
import useAvatarDataStore from "@/stores/avatarDataStore";
import useBattleDataStore from "@/stores/battleDataStore";
import useLocaleStore from "@/stores/localeStore";
import {attackTypeToString, AvatarHakushiType} from "@/types";
import { attackTypeToString, CharacterBasic } from "@/types";
import { SkillBattleInfo } from "@/types/mics";
import { useEffect, useState, useRef } from "react";
import { useTranslations } from "next-intl";
@@ -13,7 +13,7 @@ import NameAvatar from "../nameAvatar";
export default function ActionBar() {
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 { skillHistory, turnHistory, cycleIndex, waveIndex, maxWave } = useBattleDataStore();
const { listAvatar } = useAvatarDataStore();
@@ -29,7 +29,7 @@ export default function ActionBar() {
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;
if (modal) {
setSelectAvatar(avatar);
@@ -105,13 +105,13 @@ export default function ActionBar() {
skillHistory.map((turn, index) => {
const data = listAvatar.find(it => it.id === turn.avatarId.toString());
if (!data) return null;
const text = getNameChar(locale, data);
const text = getNameChar(locale, transI18n, data);
return (
<div key={index} className="h-full md:w-full">
<div
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
style={contentStyle}
@@ -121,17 +121,19 @@ export default function ActionBar() {
<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
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${data.id}.webp`}
src={`${process.env.CDN_URL}/${data.icon}`}
unoptimized
crossOrigin="anonymous"
alt={text}
width={32}
height={32}
width={125}
height={125}
className="w-8 h-8 object-contain"
/>
</div>
</div>
<NameAvatar
locale={locale}
text={getNameChar(locale, data)}
text={getNameChar(locale, transI18n, data)}
className="text-base-content text-center text-sm mt-1 font-medium"
/>
</div>
@@ -195,15 +197,17 @@ export default function ActionBar() {
<span className="font-medium text-base-content/70">{transI18n("character")}:</span>
<NameAvatar
locale={locale}
text={getNameChar(locale, selectAvatar)}
text={getNameChar(locale, transI18n, selectAvatar)}
className="font-bold"
/>
</div>
</div>
<div className="flex justify-center items-center">
<Image
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${selectAvatar.id}.webp`}
alt={getNameChar(locale, selectAvatar)}
src={`${process.env.CDN_URL}/${selectAvatar?.icon}`}
unoptimized
crossOrigin="anonymous"
alt={getNameChar(locale, transI18n, selectAvatar)}
width={80}
height={80}
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 useLocaleStore from '@/stores/localeStore';
import { AvatarHakushiType } from '@/types';
import { CharacterBasic } from '@/types';
import NameAvatar from '../nameAvatar';
import useBattleDataStore from '@/stores/battleDataStore';
import Image from 'next/image';
import { useTranslations } from 'next-intl';
interface CharacterCardProps {
data: AvatarHakushiType
data: CharacterBasic
}
export default function CharacterCard({ data }: CharacterCardProps) {
const { locale } = useLocaleStore();
const text = getNameChar(locale, data)
const { avatarDetail } = useBattleDataStore()
const transI18n = useTranslations("DataAnalysisPage");
const text = getNameChar(locale, transI18n, data)
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">
@@ -31,21 +31,27 @@ export default function CharacterCard({ data }: CharacterCardProps) {
<Image
width={376}
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"
alt="ALT"
/>
<Image
width={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"
alt={data.damageType.toLowerCase()}
/>
<Image
width={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"
alt={data.baseType.toLowerCase()}
/>

View File

@@ -24,14 +24,13 @@ export default function MultiCharLineChart() {
const [mode, setMode] = useState<1 | 2>(2);
const dataByAvatar = useDamageLinesForAll(mode);
const avatarIds = Object.keys(dataByAvatar).map(Number);
const { listAvatar } = useAvatarDataStore()
const { mapAvatar } = useAvatarDataStore()
const { locale } = useLocaleStore();
const transI18n = useTranslations("DataAnalysisPage")
const data = {
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 }) => {
return {
x: x.toFixed(2),

View File

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

View File

@@ -10,7 +10,7 @@ import NameAvatar from "../nameAvatar";
export default function EnemyBar() {
const { enemyDetail } = useBattleDataStore()
const { listEnemy } = useAvatarDataStore()
const { mapEnemy } = useAvatarDataStore()
const { locale } = useLocaleStore()
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 className="flex items-center justify-between mb-3">
<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
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}
unoptimized
crossOrigin="anonymous"
width={40}
height={40}
className="object-cover w-10 h-10 rounded-lg"
@@ -33,7 +35,7 @@ export default function EnemyBar() {
<div className="flex-1 min-w-0">
<NameAvatar
text={getNameEnemy(locale, listEnemy.find((monster) => monster.child.includes(enemy.id)))}
text={getNameEnemy(locale, mapEnemy?.[enemy.id.toString()])}
locale={locale}
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 { useTranslations } from "next-intl";
import { useState, useEffect } from "react";
import { AvatarHakushiType } from "@/types";
import { CharacterBasic } from "@/types";
import useLocaleStore from "@/stores/localeStore";
import { getNameChar } from '@/helper/getNameChar';
import SkillBarChart from "../chart/skillBarChart";
@@ -18,12 +18,12 @@ import NameAvatar from "../nameAvatar";
// import ShowCaseInfo from "../card/showCaseCard";
export default function LineupBar() {
const [selectedCharacter, setSelectedCharacter] = useState<AvatarHakushiType | undefined>(undefined);
const [selectedCharacter, setSelectedCharacter] = useState<CharacterBasic | undefined>(undefined);
const [isModalOpen, setIsModalOpen] = useState(false);
const transI18n = useTranslations("DataAnalysisPage");
const { lineup, turnHistory, dataAvatar } = useBattleDataStore();
const { listAvatar } = useAvatarDataStore();
const { listAvatar, mapAvatar } = useAvatarDataStore();
const { locale } = useLocaleStore();
const totalDamage = useCalcTotalDmgAvatar(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)
);
const handleShow = (modalId: string, item: AvatarHakushiType) => {
const handleShow = (modalId: string, item: CharacterBasic) => {
const modal = document.getElementById(modalId) as HTMLDialogElement | null;
if (modal) {
setSelectedCharacter(item);
@@ -88,7 +88,7 @@ export default function LineupBar() {
<NameAvatar
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>
</div>
@@ -145,7 +145,7 @@ export default function LineupBar() {
<NameAvatar
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"}
/>
</div>
@@ -170,8 +170,9 @@ export default function LineupBar() {
<span className="font-bold">{transI18n(selectedCharacter.baseType.toLowerCase())}</span>
{selectedCharacter.baseType && (
<Image
src={`https://api.hakush.in/hsr/UI/pathicon/${selectedCharacter.baseType.toLowerCase()}.webp`}
unoptimized
crossOrigin="anonymous"
src={`/icon/${selectedCharacter.baseType.toLowerCase()}.webp`}
className="w-6 h-6"
alt={selectedCharacter.baseType.toLowerCase()}
width={24}
@@ -185,7 +186,9 @@ export default function LineupBar() {
<p className="flex items-center space-x-2">
<span>{transI18n("element")}:</span>
<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"
alt={selectedCharacter.damageType.toLowerCase()}
width={24}
@@ -212,7 +215,9 @@ export default function LineupBar() {
<div className="flex items-center space-x-2">
<span>{transI18n("lightcones")}:</span>
<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"
alt={avatar?.Lightcone?.item_id?.toString() || ""}
width={200}
@@ -224,8 +229,10 @@ export default function LineupBar() {
<div className="grid grid-cols-3 md:flex md:flex-row w-full">
{relicIds.map(it => (
<Image
unoptimized
crossOrigin="anonymous"
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"
alt={avatar?.Lightcone?.item_id?.toString() || ""}
width={200}
@@ -243,8 +250,10 @@ export default function LineupBar() {
</div>
<Image
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${selectedCharacter.id}.webp`}
alt={getNameChar(locale, selectedCharacter)}
unoptimized
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"
width={128}
height={128}

View File

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

View File

@@ -1,38 +1,51 @@
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 {
if (!data) {
return ""
}
if (!listCurrentLanguage.hasOwnProperty(locale)) {
return ""
type TFunc = ReturnType<typeof useTranslations>
export function getNameChar(
locale: string,
t: TFunc,
data: CharacterBasic | undefined
): string {
if (!data) return "";
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguage, locale)) {
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) {
text = data.lang.get("en") ?? "";
text = data.lang["en"] ?? "";
}
if (Number(data.id) % 2 === 0 && Number(data.id) > 8000) {
text = `Female ${data.damageType} MC`
} else if (Number(data.id) > 8000) {
text = `Male ${data.damageType} MC`
if (Number(data.id) > 8000) {
text = `${t("trailblazer")}${t(data?.baseType?.toLowerCase() ?? "")}`;
}
return text
return text;
}
export function getNameEnemy(locale: string, data: EnemyHakushiType | undefined): string {
export function getNameEnemy(locale: string, data: MonsterBasic | undefined): string {
if (!data) {
return ""
}
if (!listCurrentLanguage.hasOwnProperty(locale)) {
if (!Object.prototype.hasOwnProperty.call(listCurrentLanguage, locale)) {
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) {
text = data.lang.get("en") ?? "";
text = data.lang["en"] ?? "";
}
return text
}

View File

@@ -1,6 +1,5 @@
import useSocketStore from "@/stores/socketSettingStore";
import { EnemyHakushiRawType, EnemyHakushiType } from "@/types";
import { AvatarHakushiType, AvatarHakushiRawType } from "@/types/avatar";
import { CharacterBasic, MonsterBasic } from "@/types";
import axios from 'axios';
export async function checkConnectTcpApi(): Promise<boolean> {
@@ -23,20 +22,10 @@ export async function checkConnectTcpApi(): Promise<boolean> {
return false
}
export async function getCharacterListApi(): Promise<AvatarHakushiType[]> {
export async function getCharacterListApi(): Promise<CharacterBasic[]> {
try {
const res = await axios.get<Record<string, AvatarHakushiRawType>>(
'https://api.hakush.in/hsr/data/character.json',
{
headers: {
'Content-Type': 'application/json',
},
}
);
const data = new Map(Object.entries(res.data));
return Array.from(data.entries()).map(([id, it]) => convertAvatar(id, it));
const res = await axios.get<CharacterBasic[]>("/data/character.json");
return res.data
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
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 {
const res = await axios.get<Record<string, EnemyHakushiRawType>>(
'https://api.hakush.in/hsr/data/monster.json',
{
headers: {
'Content-Type': 'application/json',
},
}
);
const data = new Map(Object.entries(res.data));
return Array.from(data.entries()).map(([id, it]) => convertMonster(id, it));
const res = await axios.get<MonsterBasic[]>("/data/monster.json");
return res.data
} catch (error: unknown) {
if (axios.isAxiosError(error)) {
console.log(`Error: ${error.response?.status} - ${error.message}`);
@@ -70,46 +49,3 @@ export async function getEnemyListApi(): Promise<EnemyHakushiType[]> {
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'
interface AvatarDataState {
listAvatar: AvatarHakushiType[];
listEnemy: EnemyHakushiType[];
setListAvatar: (list: AvatarHakushiType[]) => void;
setListEnemy: (list: EnemyHakushiType[]) => void;
listAvatar: CharacterBasic[];
mapAvatar: Record<string, CharacterBasic>;
listEnemy: MonsterBasic[];
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) => ({
listAvatar: [],
listEnemy: [],
setListAvatar: (list: AvatarHakushiType[]) => set({ listAvatar: list }),
setListEnemy: (list: EnemyHakushiType[]) => set({ listEnemy: list }),
mapAvatar: {},
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;

View File

@@ -1,15 +1,25 @@
import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware';
interface LocaleState {
locale: string;
theme: string
setTheme: (newTheme: string) => void;
setLocale: (newLocale: string) => void;
}
const useLocaleStore = create<LocaleState>((set) => ({
const useLocaleStore = create<LocaleState>()(persist(
(set) => ({
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;

View File

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

View File

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