File size: 3,258 Bytes
2de9430
bfe0820
2de9430
a02845e
2de9430
bfe0820
a02845e
5c95266
a02845e
2de9430
 
 
a02845e
 
eead5a7
5c95266
bfe0820
 
2de9430
 
 
 
 
bfe0820
2de9430
 
 
 
 
 
 
 
 
 
bfe0820
2de9430
 
 
 
bfe0820
2de9430
 
 
 
 
a02845e
 
2de9430
bfe0820
2de9430
 
eead5a7
2de9430
 
 
bfe0820
2de9430
 
 
 
 
 
 
 
 
 
bfe0820
 
 
 
 
 
2de9430
bfe0820
 
2de9430
bfe0820
2de9430
bfe0820
 
 
2de9430
bfe0820
2de9430
 
 
 
 
5c95266
2de9430
bfe0820
a02845e
bfe0820
2de9430
bfe0820
 
 
 
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
import React, { useCallback, useEffect, useState } from "react";
import { Outlet, useLocation } from "react-router-dom";
import { AnimatePresence, motion } from "framer-motion";
import { TopBar } from "./TopBar";
import { Sidebar, SIDEBAR_COLLAPSED, SIDEBAR_EXPANDED } from "./Sidebar";
import { BottomNavigation } from "./BottomNavigation";
import { useIsMobile } from "../../hooks/useIsMobile";
import { useCommodityPrices, useMarketQuotes } from "../../hooks/useMarketData";

const LS_KEY = "tm_sidebar_collapsed";
const MAIN_TRANSITION = "margin-left 0.3s cubic-bezier(0.4,0,0.2,1)";

export function AppLayout() {
  const isMobile = useIsMobile();
  const { commodities, loading: commodityLoading } = useCommodityPrices();
  const marketQuotes = useMarketQuotes();
  const location = useLocation();

  // Collapsed state — persisted in localStorage
  const [collapsed, setCollapsed] = useState(() => {
    try { return localStorage.getItem(LS_KEY) === "true"; }
    catch { return false; }
  });

  // Mobile drawer open state
  const [mobileOpen, setMobileOpen] = useState(false);

  const toggleCollapsed = useCallback(() => {
    setCollapsed((c) => {
      const next = !c;
      try { localStorage.setItem(LS_KEY, String(next)); } catch {}
      return next;
    });
  }, []);

  // Close mobile drawer on route change
  useEffect(() => { setMobileOpen(false); }, [location.pathname]);

  // Sync CSS variable for any consumers that read it
  useEffect(() => {
    const w = isMobile ? 0 : collapsed ? SIDEBAR_COLLAPSED : SIDEBAR_EXPANDED;
    document.documentElement.style.setProperty("--sidebar-width", `${w}px`);
  }, [isMobile, collapsed]);

  const sidebarW = isMobile ? 0 : collapsed ? SIDEBAR_COLLAPSED : SIDEBAR_EXPANDED;

  return (
    <div style={{ minHeight: "100vh", background: "var(--bg-primary)" }}>

      {/* Top bar — full width, always on top */}
      <TopBar
        commodities={commodities}
        isMobile={isMobile}
        onMenuClick={() => setMobileOpen(true)}
      />

      {/* Sidebar */}
      <Sidebar
        collapsed={collapsed}
        onToggle={toggleCollapsed}
        isMobile={isMobile}
        mobileOpen={mobileOpen}
        onMobileClose={() => setMobileOpen(false)}
      />

      {/* Main content */}
      <main
        id="main-content"
        tabIndex={-1}
        role="main"
        style={{
          marginTop: 52,
          marginLeft: sidebarW,
          minHeight: "calc(100vh - 52px)",
          padding: isMobile ? "16px 14px 80px" : "28px 32px 48px",
          transition: isMobile ? "none" : MAIN_TRANSITION,
          boxSizing: "border-box",
          willChange: "margin-left",
        }}
      >
        <AnimatePresence mode="wait" initial={false}>
          <motion.div
            key={location.pathname}
            initial={{ opacity: 0, y: 8 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{    opacity: 0, y: -4 }}
            transition={{ duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
          >
            <Outlet context={{ commodities, commodityLoading, marketQuotes, isMobile }} />
          </motion.div>
        </AnimatePresence>
      </main>

      {/* Bottom nav — mobile only */}
      {isMobile && <BottomNavigation />}
    </div>
  );
}