File size: 4,624 Bytes
34b0226
013ff92
00a4363
aeea10c
00a4363
aeea10c
00a4363
aeea10c
00a4363
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34b0226
 
 
 
 
 
 
 
 
 
00a4363
 
013ff92
34b0226
 
 
 
 
 
 
 
 
 
00a4363
013ff92
 
 
 
00a4363
 
aeea10c
 
 
 
 
 
 
8e2a6eb
aeea10c
 
 
8e2a6eb
aeea10c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23cef78
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
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,
  ConstellationIcon,
  HelmetIcon,
  LaunchpadIcon,
  LaunchWindowIcon,
  OrbitIcon,
  PlanetIcon,
  RadarIcon,
  RocketIcon,
  SatelliteDishIcon,
} from "./icons/Icons";

// Theme in the icon, clarity in the label: the plain label always shows.
const NAV = [
  { to: "/app", label: "Mission Control", icon: RadarIcon, end: true },
  { to: "/app/notebooks", label: "Notebooks", icon: PlanetIcon },
  { to: "/app/sources", label: "Sources", icon: SatelliteDishIcon },
  { to: "/app/map", label: "Topic map", icon: ConstellationIcon },
  { to: "/app/voice", label: "Voice profile", icon: HelmetIcon },
  { to: "/app/studio", label: "Video and audio", icon: LaunchWindowIcon },
  { to: "/app/launch-kit", label: "Launch Kit", icon: RocketIcon },
  { to: "/app/launchpad", label: "Launchpad", icon: LaunchpadIcon },
  { to: "/app/archive", label: "Archive", icon: AsteroidIcon },
  { to: "/app/resurface", label: "Resurfacing", icon: CometIcon },
  { to: "/app/settings", label: "Settings", icon: OrbitIcon },
];

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 }) => (
              <NavLink key={to} to={to} end={end} title={collapsed ? label : undefined} className={({ isActive }) => `side-link${isActive ? " active" : ""}`}>
                <Icon />
                <span className="side-label">{label}</span>
              </NavLink>
            ))}
          </nav>
          <FuelGauge collapsed={collapsed} />
          <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>
  );
}