update for v4

This commit is contained in:
2025-04-27 13:53:19 +07:00
parent e1e876a4e8
commit 6a9731b882
15 changed files with 470 additions and 170 deletions

View File

@@ -44,14 +44,14 @@ export default function Home() {
}, [expandedCharts]);
return (
<div className="flex flex-col h-full w-full mt-5 min-h-[74vh] bg-base-100 font-[family-name:var(--font-geist-sans)]">
<div className="flex flex-col px-2 h-full w-full mt-5 min-h-[74vh] bg-base-100 font-[family-name:var(--font-geist-sans)]">
<div className="h-full">
<div className="grid grid-cols-12 gap-2 lg:gap-3 h-full min-h-full">
<div className="col-span-12 md:col-span-3 lg:col-span-2 xl:col-span-2 h-full">
<ActionBar />
</div>
<div className="col-span-12 md:col-span-6 lg:col-span-8 xl:col-span-8 flex flex-col h-full">
<div className="col-span-12 md:col-span-6 lg:col-span-8 xl:col-span-8 max-h-[90vh] flex flex-col h-full overflow-auto">
<div className="grid grid-cols-3 gap-2 mb-3">
<div className="p-2 text-base lg:text-lg xl:text-xl rounded bg-primary text-primary-content text-center shadow-md">
{transI18n("totalDamage")}: {Number(totalDamage).toFixed(2)}
@@ -64,7 +64,7 @@ export default function Home() {
</div>
</div>
<div className="bg-base-200 rounded-lg p-2 shadow-md flex-grow">
<div className="rounded-lg p-2 shadow-md flex-grow">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{expandedCharts.includes('chart1') ? (
@@ -112,7 +112,7 @@ export default function Home() {
onClick={() => setModeLine(m as 0 | 1)}
className={`btn btn-sm ${modeLine === m ? "btn-accent" : "btn-ghost"}`}
>
{transI18n("style")} {m}
{transI18n("type")} {m}
</button>
))}
</div>
@@ -120,7 +120,7 @@ export default function Home() {
</div>
{expandedCharts.includes('chart3') ? (
<div key="chart3-expanded" className="lg:col-span-2 bg-base-200 rounded-lg p-2 shadow-md relative">
<div key="chart3-expanded" className="lg:col-span-2 max-h-[70vh] bg-base-200 rounded-lg p-2 shadow-md relative">
<div className="absolute top-2 left-2 z-10">
<button
className="btn btn-sm btn-circle btn-ghost"
@@ -132,7 +132,7 @@ export default function Home() {
<DamagePercentChartForAll />
</div>
) : (
<div key="chart3-normal" className="bg-base-200 rounded-lg p-2 shadow-md relative">
<div key="chart3-normal" className="bg-base-200 max-h-[40vh] rounded-lg p-2 shadow-md relative">
<div className="absolute top-2 left-2 z-10">
<button
className="btn btn-sm btn-circle btn-ghost"
@@ -164,7 +164,7 @@ export default function Home() {
onClick={() => setModeBar(m as 0 | 1 | 2)}
className={`btn btn-sm ${modeBar === m ? "btn-accent" : "btn-ghost"}`}
>
{transI18n("style")} {m}
{transI18n("type")} {m}
</button>
))}
</div>

View File

@@ -19,13 +19,9 @@ export default function ActionBar() {
const transI18n = useTranslations("DataAnalysisPage");
const turnListRef = useRef<HTMLDivElement>(null);
const parallelogramStyle: React.CSSProperties = {
transform: 'skew(-9deg)',
overflow: 'hidden',
};
const contentStyle: React.CSSProperties = {
transform: 'skew(9deg)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
@@ -71,7 +67,7 @@ export default function ActionBar() {
}, [turnHistory.length]);
return (
<div className="p-4 rounded-lg shadow-lg w-full h-full min-h-[74vh]">
<div className="p-4 md:p-1 rounded-lg shadow-lg w-full h-full">
<motion.h2
className="text-center text-xl lg:text-2xl mb-2 font-bold text-transparent bg-clip-text bg-gradient-to-r from-pink-500 via-purple-500 to-cyan-500"
initial={{ opacity: 0, y: -20 }}
@@ -82,43 +78,43 @@ export default function ActionBar() {
</motion.h2>
<div
ref={turnListRef}
className="flex px-2 w-full max-h-[90vh] pt-2 border-t-2 border-accent overflow-y-auto custom-scrollbar overflow-x-hidden"
className="flex md:block px-2 md:px-0 w-full pt-2 border-t-2 border-accent overflow-x-auto md:overflow-x-hidden md:overflow-y-auto max-h-[90vh] custom-scrollbar"
>
<style jsx>{`
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: hsl(var(--p)) hsl(var(--b3));
scrollbar-width: thin;
scrollbar-color: hsl(var(--p)) hsl(var(--b3));
}
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
width: 8px;
height: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: hsl(var(--b3));
border-radius: 10px;
background: hsl(var(--b3));
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: hsl(var(--p));
border-radius: 10px;
background: hsl(var(--p));
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: hsl(var(--pf));
background: hsl(var(--pf));
}
.custom-scrollbar::-webkit-scrollbar-button {
display: none;
height: 0;
width: 0;
display: none;
height: 0;
width: 0;
}
`}</style>
<div className="w-full h-fit grid grid-cols-1 gap-1 ">
<div className="flex flex-nowrap md:grid md:grid-cols-1 gap-2 w-fit md:w-full">
{turnHistory.length === 0 ? (
<div className="flex items-center justify-center h-full">
<div className="flex items-center justify-center h-full w-full">
<p className="text-base-content opacity-50">{transI18n("noTurns")}</p>
</div>
) : (
@@ -128,45 +124,48 @@ export default function ActionBar() {
const text = getNameChar(locale, data);
return (
<div key={index}>
<div key={index} className="h-full md:w-full">
<div
onClick={() => handleShow("action_detail_modal", data, turn)}
style={parallelogramStyle}
className="flex border bg-base-100 w-full hover:bg-base-200 transition-colors duration-200 border-cyan-400 border-l-4 cursor-pointer"
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"
>
<div
style={contentStyle}
className="flex flex-col items-center justify-center py-2 px-3"
className="lg:col-span-1 grid grid-cols-1 items-center justify-center py-2"
>
<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">
<img
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${data.id}.webp`}
alt={text}
loading="lazy"
className="w-8 h-8 object-contain"
/>
</div>
</div>
<div className="text-base-content text-sm mt-1 font-medium">{text}</div>
<div className="text-base-content text-center text-sm mt-1 font-medium">{getNameChar(locale, data)}</div>
</div>
<div className="flex flex-col justify-center gap-2 px-3 py-2">
<div className="text-primary text-xs">
{`${transI18n("useSkill")}: ${turn.skillType}`}
<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">
{`${transI18n("useSkill")}: ${transI18n(turn.skillType.toLowerCase())}`}
</div>
<div className="text-primary text-xs">
<div className="text-primary text-xs max-w-full">
{`${transI18n("totalDamage")}: ${turn.totalDamage.toFixed(2)}`}
</div>
</div>
</div>
</div>
);
})
)}
</div>
</div>
{/* Character Detail Modal */}
<dialog id="action_detail_modal" className="modal modal-bottom sm:modal-middle backdrop-blur-sm">
<dialog id="action_detail_modal" className="modal sm:modal-middle backdrop-blur-sm">
<div className="modal-box w-11/12 max-w-7xl bg-base-100 text-base-content border border-purple-500/50 shadow-lg shadow-purple-500/20">
<div className="sticky top-0 z-10">
<motion.button
@@ -181,7 +180,7 @@ export default function ActionBar() {
<div className="border-b border-purple-500/30 px-6 py-4 mb-4">
<h3 className="font-bold text-2xl text-transparent bg-clip-text bg-gradient-to-r from-pink-400 to-cyan-400">
{transI18n("turnDetail").toUpperCase()}
{transI18n("turnDetail").toUpperCase()}
</h3>
</div>
@@ -207,6 +206,7 @@ export default function ActionBar() {
</div>
<div className="flex justify-center items-center">
<img
loading="lazy"
src={`https://api.hakush.in/hsr/UI/avatarshopicon/${selectAvatar.id}.webp`}
alt={getNameChar(locale, selectAvatar)}
className="h-20 w-20 object-cover rounded-full border-2 border-purple-500 shadow-lg shadow-purple-500/20"

View File

@@ -2,6 +2,7 @@
import { exportBattleData, importBattleData } from "@/helper";
import { useChangeTheme } from "@/hooks/useChangeTheme";
import { checkConnectTcpApi } from "@/lib/api";
import { listCurrentLanguage } from "@/lib/constant";
import { connectSocket, disconnectSocket, getSocket, isSocketConnected } from "@/lib/socket";
import useBattleDataStore from "@/stores/battleDataStore";
import useLocaleStore from "@/stores/localeStore";
@@ -38,7 +39,9 @@ export default function Header() {
const [message, setMessage] = useState({ text: '', type: '' });
const [isModalOpen, setIsModalOpen] = useState(false);
useEffect(() => {
console.log(navigator.language.slice(0, 2))
const cookieLocale = document.cookie.split("; ")
.find((row) => row.startsWith("MYNEXTAPP_LOCALE"))
?.split("=")[1];
@@ -47,8 +50,8 @@ export default function Header() {
setLocale(cookieLocale)
} else {
let browserLocale = navigator.language.slice(0, 2);
const listCurrentLanguage = ["jp", "kr", "en", "vi", "zh", "cn"]
if(!listCurrentLanguage.includes(browserLocale)) {
if (!listCurrentLanguage.hasOwnProperty(browserLocale)) {
browserLocale = "en"
}
setLocale(browserLocale);
@@ -167,15 +170,33 @@ export default function Header() {
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-10 mt-3 w-52 p-2 shadow-md border border-base-200"
>
<li>
<button
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
>
{transI18n("loadData")}
</button>
<>
<input
type="file"
accept="application/json"
id="battle-data-upload"
className="hidden"
onChange={(e) => {
const file = e.target.files?.[0];
if (file) {
importBattleData(file, loadBattleDataFromJSON)
.then(() => console.log('Data loaded'))
.catch(err => alert('Failed to load data: ' + err.message));
}
}}
/>
<button
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => document.getElementById('battle-data-upload')?.click()}
>
{transI18n("loadData")}
</button>
</>
</li>
<li>
<button
className="px-3 py-2 hover:bg-base-200 rounded-md transition-all duration-200 font-medium"
onClick={() => exportBattleData({ totalAV, totalDamage, turnHistory, damagePerAV, lineup } as BattleDataStateJson)}
>
{transI18n("exportData")}
</button>
@@ -193,7 +214,7 @@ export default function Header() {
{/* Logo */}
<a className=" flex flex-col items-start text-left gap-0 hover:scale-105 px-2">
<a className="hidden sm:grid sm:grid-cols-1 items-start text-left gap-0 hover:scale-105 px-2">
<h1 className="text-xl font-bold">
<span className="text-emerald-500">Firefly Analy</span>
<span className="bg-clip-text text-transparent bg-gradient-to-r from-emerald-400 via-orange-500 to-red-500">
@@ -256,12 +277,13 @@ export default function Header() {
<div className="navbar-end gap-2">
<div className="px-2">
<div className="flex items-center space-x-2 p-1.5 rounded-full shadow-md">
<div className={`text-sm italic ${status ? 'text-green-500' : 'text-red-500'}`}>
<div className={`hidden lg:block text-sm italic ${status ? 'text-green-500' : 'text-red-500'}`}>
{status ? transI18n("connected") : transI18n("unconnected")}
</div>
<div className={`w-3 h-3 rounded-full ${status ? 'bg-green-500' : 'bg-red-500'}`}></div>
</div>
</div>
{/* Language selector - REFINED */}
<div className="dropdown dropdown-end">
<div className="flex items-center gap-1 border border-base-300 rounded text-sm px-1.5 py-0.5 hover:bg-base-200 cursor-pointer transition-all duration-200">
@@ -274,11 +296,10 @@ export default function Header() {
value={locale}
onChange={(e) => changeLocale(e.target.value)}
>
<option value="vi">VI</option>
<option value="en">EN</option>
<option value="cn">CN</option>
<option value="jp">JP</option>
<option value="kr">KR</option>
{Object.entries(listCurrentLanguage).map(([key, value]) => (
<option key={key} value={key}>{value}</option>
))}
</select>
</div>
</div>
@@ -347,7 +368,7 @@ export default function Header() {
{/* GitHub Link */}
<Link
className='btn btn-ghost btn-sm btn-circle hover:bg-base-200 transition-all duration-200'
className='hidden sm:block btn btn-ghost btn-sm btn-circle hover:bg-base-200 transition-all duration-200'
href={"https://github.com/AzenKain/SR-Analysis"}
target="_blank"
rel="noopener noreferrer"

View File

@@ -66,7 +66,7 @@ export default function LineupBar() {
}, [isModalOpen]);
return (
<div className="p-4 rounded-lg shadow-lg w-full h-full min-h-[74vh]">
<div className="p-4 md:p-1 rounded-lg shadow-lg w-full h-full">
<motion.h2
className="text-center text-xl lg:text-2xl pb-2 font-bold text-transparent bg-clip-text bg-gradient-to-r from-pink-500 via-purple-500 to-cyan-500"
initial={{ opacity: 0, y: -20 }}
@@ -82,39 +82,40 @@ export default function LineupBar() {
<p className="text-base-content opacity-50">{transI18n("noCharactersInLineup")}</p>
</div>
) : (
<div className="h-full w-full overflow-y-auto overflow-x-hidden custom-scrollbar rounded-lg">
<div className="h-full w-full overflow-x-auto md:overflow-x-hidden md:overflow-y-auto custom-scrollbar rounded-lg">
<style jsx>{`
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: hsl(var(--p)) hsl(var(--b3));
}
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: hsl(var(--p)) hsl(var(--b3));
}
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.custom-scrollbar::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: hsl(var(--b3));
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: hsl(var(--b3));
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: hsl(var(--p));
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: hsl(var(--p));
border-radius: 10px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: hsl(var(--pf));
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: hsl(var(--pf));
}
.custom-scrollbar::-webkit-scrollbar-button {
display: none;
height: 0;
width: 0;
}
`}</style>
<div className="grid grid-cols-1 w-full justify-items-center items-start">
.custom-scrollbar::-webkit-scrollbar-button {
display: none;
height: 0;
width: 0;
}
`}</style>
<div className="flex flex-nowrap md:grid md:grid-cols-1 w-fit md:w-full justify-items-center items-start gap-2">
{lineupAvatars.map((item, index) => (
<motion.div
key={item.id}
@@ -122,7 +123,7 @@ export default function LineupBar() {
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
whileHover={{ scale: 1.05 }}
className="cursor-pointer"
className="cursor-pointer flex-shrink-0 md:w-full justify-items-center"
onClick={() => handleShow("character_detail_modal", item)}
>
<CharacterCard data={item} />
@@ -130,6 +131,7 @@ export default function LineupBar() {
))}
</div>
</div>
)}
{/* Character Detail Modal */}
@@ -165,7 +167,7 @@ export default function LineupBar() {
<div className="grid grid-cols-1 sm:grid-cols-2">
<div className="flex flex-col space-y-2 relative">
<p>
{transI18n("id")}: <span className="font-bold">{selectedCharacter.id}</span>
{transI18n("id")}: <span className="font-bold">{selectedCharacter.id}</span>
</p>
<p className="flex items-center space-x-2">
<span>{transI18n("path")}:</span>
@@ -180,7 +182,7 @@ export default function LineupBar() {
)}
</p>
<p>
{transI18n("rarity")}: <span className="font-bold">{selectedCharacter.rank === "CombatPowerAvatarRarityType5" ? "5*" : "4*"}</span>
{transI18n("rarity")}: <span className="font-bold">{selectedCharacter.rank === "CombatPowerAvatarRarityType5" ? "5*" : "4*"}</span>
</p>
<p className="flex items-center space-x-2">
<span>{transI18n("element")}:</span>
@@ -232,7 +234,7 @@ export default function LineupBar() {
onClick={() => setModeLine(m as 0 | 1)}
className={`btn btn-sm ${modeLine === m ? "btn-accent" : "btn-ghost"}`}
>
{transI18n("style")} {m}
{transI18n("type")} {m}
</button>
))}
</div>
@@ -256,7 +258,7 @@ export default function LineupBar() {
onClick={() => setModeBar(m as 0 | 1 | 2)}
className={`btn btn-sm ${modeBar === m ? "btn-accent" : "btn-ghost"}`}
>
{transI18n("style")} {m}
{transI18n("type")} {m}
</button>
))}
</div>

View File

@@ -1,3 +1,4 @@
import { listCurrentLanguage } from "@/lib/constant";
import { AvatarType } from "@/types";
@@ -5,7 +6,11 @@ export function getNameChar(locale: string, data: AvatarType | undefined): strin
if (!data) {
return ""
}
let text = data.lang.get(locale) ?? "";
if (!listCurrentLanguage.hasOwnProperty(locale)) {
return ""
}
let text = data.lang.get(listCurrentLanguage[locale as keyof typeof listCurrentLanguage].toLowerCase()) ?? "";
if (!text) {
text = data.lang.get("en") ?? "";
}

7
src/lib/constant.ts Normal file
View File

@@ -0,0 +1,7 @@
export const listCurrentLanguage = {
ja: "JP",
ko: "KR",
en: "US",
vi: "VN",
zh: "CN"
};

View File

@@ -77,12 +77,16 @@ export const connectSocket = (): Socket => {
socket.on("SetBattleLineup", (json) => onSetBattleLineupService(JSON.parse(json)));
socket.on("TurnEnd", (json) => onTurnEndService(JSON.parse(json)));
socket.on("OnTurnEnd", (json) => onTurnEndService(JSON.parse(json)));
socket.on("OnUseSkill", (json) => onUseSkillService(JSON.parse(json)));
socket.on("OnKill", (json) => onKillService(JSON.parse(json)));
socket.on("OnDamage", (json) => onDamageService(JSON.parse(json)));
socket.on('BattleBegin', () => onBattleBegin());
socket.on('OnBattleBegin', () => onBattleBegin());
socket.on('TurnBegin', (json) => onTurnBeginService(JSON.parse(json)));
socket.on('OnTurnBegin', (json) => onTurnBeginService(JSON.parse(json)));
socket.on('BattleEnd', (json) => onBattleEndService(JSON.parse(json)));
socket.on('OnBattleEnd', (json) => onBattleEndService(JSON.parse(json)));
socket.on("Error", (msg: string) => {
console.error("Server Error:", msg);
@@ -104,12 +108,16 @@ export const disconnectSocket = (): void => {
if (socket) {
socket.off("SetBattleLineup", (json) => onSetBattleLineupService(JSON.parse(json)));
socket.off("TurnEnd", (json) => onTurnEndService(JSON.parse(json)));
socket.off("OnTurnEnd", (json) => onTurnEndService(JSON.parse(json)));
socket.off("OnUseSkill", (json) => onUseSkillService(JSON.parse(json)));
socket.off("OnKill", (json) => onKillService(JSON.parse(json)));
socket.off("OnDamage", (json) => onDamageService(JSON.parse(json)));
socket.off('BattleBegin', () => onBattleBegin());
socket.off('OnBattleBegin', () => onBattleBegin());
socket.off('TurnBegin', (json) => onTurnBeginService(JSON.parse(json)));
socket.off('OnTurnBegin', (json) => onTurnBeginService(JSON.parse(json)));
socket.off('BattleEnd', (json) => onBattleEndService(JSON.parse(json)));
socket.off('OnBattleEnd', (json) => onBattleEndService(JSON.parse(json)));
socket.offAny();
socket.disconnect();
useSocketStore.getState().setStatus(false);