File size: 3,531 Bytes
3370310
3e1734c
9b53f70
3e1734c
 
 
9b53f70
3370310
 
 
 
 
 
 
 
 
9b53f70
 
 
 
 
 
3e1734c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9b53f70
 
 
 
 
 
 
 
 
 
 
3e1734c
 
 
 
3370310
 
 
 
3e1734c
3370310
3e1734c
 
 
 
 
 
 
 
3370310
3e1734c
35768b0
3e1734c
 
 
 
 
 
 
35768b0
3e1734c
 
9b53f70
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
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>
  );
}