Spaces:
Running
Running
Download frontend/src/components/Layout.jsx from tntgroup/website_content: direct link, hf CLI and curl.
- Browser
- Download file 4.11 kB
-
https://huggingface.co/spaces/tntgroup/website_content/resolve/main/frontend/src/components/Layout.jsx
- Command line
-
hf download hf://spaces/tntgroup/website_content/frontend/src/components/Layout.jsx
-
curl -L -o Layout.jsx https://huggingface.co/spaces/tntgroup/website_content/resolve/main/frontend/src/components/Layout.jsx
4.11 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"; | |
| import track, { ROUTE_FEATURE } from "../lib/track"; | |
| // 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]); | |
| // Theo dõi người dùng: bật một lần, rồi mỗi lần đổi trang là mở "task" mới | |
| // cho tool đó (task cũ chưa có đầu ra sẽ tự ghi là bỏ dở). | |
| useEffect(() => { | |
| if (authorized) track.initTracking(); | |
| }, [authorized]); | |
| useEffect(() => { | |
| if (!authorized) return; | |
| const feature = ROUTE_FEATURE[location.pathname]; | |
| if (feature) track.featureOpen(feature, { entry: "router" }); | |
| }, [authorized, location.pathname]); | |
| const handleLogout = () => { | |
| track.abandon("logout"); | |
| track.flush(); | |
| 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> | |
| ); | |
| } | |