tnt-group / frontend /src /components /Layout.jsx
titusgiap's picture
perf: queue theo loại + code-split FE + streaming upload cho nhiều user
3370310
Raw
History Blame Contribute Delete
3.53 kB
import { useEffect, Suspense } from "react";
import { NavLink, useLocation, useBlocker, Outlet, useNavigate } from "react-router-dom";
import Sidebar from "./Sidebar";
import { useVideoGen } from "../stores/videoGenStore";
import { useDirty } from "../stores/dirtyStore";
import { useAuth } from "../stores/authStore";
// Fallback nhẹ khi chunk trang đang tải (code-splitting).
function PageFallback() {
return (
<div style={{ padding: 40, color: "var(--muted)", fontSize: 14 }}>
Đang tải công cụ…
</div>
);
}
const tabs = [
{ to: "/", label: "Trang chủ" },
{ to: "/huong-dan", label: "Hướng dẫn" },
{ to: "/ho-tro", label: "Hỗ trợ" },
];
export default function Layout() {
const location = useLocation();
const navigate = useNavigate();
const { authorized, user, logout } = useAuth();
useEffect(() => {
if (!authorized) navigate("/dang-nhap", { replace: true });
}, [authorized]);
const handleLogout = () => {
logout();
navigate("/dang-nhap");
};
const isVideoGen = location.pathname === "/tao-video";
const { connected, url } = useVideoGen();
const { dirty, message } = useDirty();
const blocker = useBlocker(
({ currentLocation, nextLocation }) =>
dirty && currentLocation.pathname !== nextLocation.pathname
);
useEffect(() => {
if (blocker.state === "blocked") {
const proceed = window.confirm(
message || "Bạn có chắc muốn rời khỏi công cụ này? Tiến trình hiện tại sẽ bị mất."
);
if (proceed) {
blocker.proceed();
} else {
blocker.reset();
}
}
}, [blocker.state, message]);
return (
<div className="shell">
<Sidebar />
<div className="main">
<div className="topbar">
{tabs.map((t) => (
<NavLink key={t.to} to={t.to} end className={({ isActive }) => "tab" + (isActive ? " active" : "")}>
{t.label}
</NavLink>
))}
<div className="spacer" />
<span style={{ fontSize: 12, color: "var(--muted)" }}>{user}</span>
<div className="avatar" style={{ cursor: "pointer" }} onClick={handleLogout} title="Đăng xuất">TNT</div>
</div>
{/* Chỉ MỘT <Outlet> được render tại một thời điểm (cái còn lại là `false`)
-> trang route chỉ mount 1 lần, không còn chạy đôi effect/poll/upload.
iframe Gradio LUÔN nằm trong cây (ngoài điều kiện) để giữ phiên kết nối
khi chuyển tool rồi quay lại /tao-video. */}
<div className="content" style={{ display: isVideoGen ? "none" : "block" }}>
<Suspense fallback={<PageFallback />}>{!isVideoGen && <Outlet />}</Suspense>
</div>
<div style={{
display: isVideoGen ? "flex" : "none",
flexDirection: "column",
flex: 1,
minHeight: 0,
}}>
<Suspense fallback={<PageFallback />}>{isVideoGen && <Outlet />}</Suspense>
<iframe
src={connected && url ? url : "about:blank"}
style={{
flex: 1,
width: "100%",
border: "none",
display: connected ? "block" : "none",
}}
allow="clipboard-read; clipboard-write; fullscreen"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups"
title="Gradio Console"
/>
</div>
</div>
</div>
);
}