Spaces:
Runtime error
Runtime error
File size: 5,019 Bytes
34b0226 013ff92 00a4363 aeea10c 00a4363 aeea10c 00a4363 aeea10c 00a4363 070c931 00a4363 070c931 3c705c5 00a4363 3c705c5 070c931 3c705c5 00a4363 34b0226 00a4363 013ff92 34b0226 00a4363 013ff92 00a4363 aeea10c 8e2a6eb aeea10c 8e2a6eb aeea10c 23cef78 aeea10c 3c705c5 aeea10c 3c705c5 aeea10c 00a4363 | 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 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 | import { useState } from "react";
import { NavLink, Outlet, Navigate, useLocation } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";
import { UpgradeProvider } from "../hooks/useUpgrade";
import Logo from "./Logo";
import { FuelGauge, NudgeDock } from "./Nudges";
import SkyCanvas from "./SkyCanvas";
import UpgradeModal from "./UpgradeModal";
import {
AsteroidIcon,
CometIcon,
HelmetIcon,
LaunchpadIcon,
LaunchWindowIcon,
OrbitIcon,
PlanetIcon,
RadarIcon,
SatelliteDishIcon,
} from "./icons/Icons";
// Theme in the icon, clarity in the label: the plain label always shows.
// Eight stops: related pages share one entry (`also` keeps it lit on them).
const NAV: { to: string; label: string; icon: typeof RadarIcon; end?: boolean; also?: string[] }[] = [
{ to: "/app", label: "Home", icon: RadarIcon, end: true },
{ to: "/app/sources", label: "Sources", icon: SatelliteDishIcon, also: ["/app/map"] },
{ to: "/app/notebooks", label: "Notebooks", icon: PlanetIcon },
{ to: "/app/studio", label: "Create", icon: LaunchWindowIcon, also: ["/app/launch-kit"] },
{ to: "/app/videos", label: "Videos", icon: CometIcon },
{ to: "/app/launchpad", label: "Launchpad", icon: LaunchpadIcon, also: ["/app/resurface"] },
{ to: "/app/archive", label: "Library", icon: AsteroidIcon },
{ to: "/app/voice", label: "Voice", icon: HelmetIcon },
];
const COLLAPSED_KEY = "ns_sidebar_collapsed";
function readCollapsed(): boolean {
try {
return localStorage.getItem(COLLAPSED_KEY) === "1";
} catch {
return false;
}
}
export default function AppShell() {
const { user, loading, logout } = useAuth();
const location = useLocation();
const [collapsed, setCollapsed] = useState(readCollapsed);
const toggle = () =>
setCollapsed((c) => {
try {
localStorage.setItem(COLLAPSED_KEY, c ? "0" : "1");
} catch {
/* private mode: remember for this visit only */
}
return !c;
});
if (loading) return <div className="boot"><div className="orbit-loader"><span /></div></div>;
if (!user) {
const next = new URLSearchParams({ next: location.pathname + location.search });
return <Navigate to={`/auth?${next}`} replace />;
}
return (
<UpgradeProvider>
<div className={`shell${collapsed ? " collapsed" : ""}`}>
<SkyCanvas intensity={0.35} />
<aside className="sidebar">
<div className="sidebar-top">
{collapsed ? (
<button type="button" className="sidebar-brand sidebar-brand-btn" onClick={toggle} aria-label="Expand sidebar" title="Expand sidebar">
<Logo size={28} withWordmark={false} />
</button>
) : (
<NavLink to="/" className="sidebar-brand">
<Logo size={28} withWordmark={false} />
</NavLink>
)}
<button
type="button"
className="icon-btn sidebar-toggle"
onClick={toggle}
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
aria-expanded={!collapsed}
title={collapsed ? "Expand sidebar" : "Collapse sidebar"}
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<rect x="3.5" y="4.5" width="17" height="15" rx="2.5" />
<path d="M9 4.5v15" />
<path d={collapsed ? "M13 10l2 2-2 2" : "M15 10l-2 2 2 2"} />
</svg>
</button>
</div>
<nav aria-label="App">
{NAV.map(({ to, label, icon: Icon, end, also }) => (
<NavLink
key={to}
to={to}
end={end}
title={collapsed ? label : undefined}
className={({ isActive }) => `side-link${isActive || also?.some((p) => location.pathname.startsWith(p)) ? " active" : ""}`}
>
<Icon />
<span className="side-label">{label}</span>
</NavLink>
))}
</nav>
<FuelGauge collapsed={collapsed} />
<NavLink to="/app/settings" title={collapsed ? "Settings" : undefined} className={({ isActive }) => `side-link side-settings${isActive ? " active" : ""}`}>
<OrbitIcon />
<span className="side-label">Settings</span>
</NavLink>
<div className="sidebar-user">
<span className="mono muted side-label">{user.email}</span>
<button className="link-btn" onClick={logout} title="Sign out">
Sign out
</button>
</div>
</aside>
<main className="shell-main">
<Outlet />
</main>
</div>
{/* Outside .shell: its children get position: relative, and these are fixed overlays. */}
<NudgeDock />
<UpgradeModal />
</UpgradeProvider>
);
}
|