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>
);
}
|