feat(ui): wire data.jsx to real API; add loading gate in app.jsx: app.jsx

This commit is contained in:
Zac Gaetano 2026-05-22 10:02:55 -04:00
parent 98025001e8
commit 7dda7cc89c

View file

@ -1,164 +1,113 @@
// app.jsx main shell wiring all screens together + tweaks
// app.jsx main shell
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
"accent": "#5B7CFA",
"density": "comfortable",
"gridSize": "md",
"showSearch": true,
"sidebarMode": "expanded"
}/*EDITMODE-END*/;
const ACCENT = '#5B7CFA';
function App() {
const [route, setRoute] = React.useState("home");
const [route, setRoute] = React.useState('home');
const [openAsset, setOpenAsset] = React.useState(null);
const [openProject, setOpenProject] = React.useState(null);
const [showNewRecorder, setShowNewRecorder] = React.useState(false);
const [t, setTweak] = (window.useTweaks ? window.useTweaks(TWEAK_DEFAULTS) : [TWEAK_DEFAULTS, () => {}]);
const [dataReady, setDataReady] = React.useState(false);
React.useEffect(() => {
document.documentElement.style.setProperty("--accent", t.accent);
document.documentElement.style.setProperty("--accent-soft", hexToRgba(t.accent, 0.14));
document.documentElement.style.setProperty("--accent-soft-2", hexToRgba(t.accent, 0.22));
document.documentElement.style.setProperty("--accent-text", lighten(t.accent, 0.25));
document.documentElement.style.setProperty("--accent-hover", lighten(t.accent, 0.08));
}, [t.accent]);
document.documentElement.style.setProperty('--accent', ACCENT);
document.documentElement.style.setProperty('--accent-soft', hexToRgba(ACCENT, 0.14));
document.documentElement.style.setProperty('--accent-soft-2', hexToRgba(ACCENT, 0.22));
document.documentElement.style.setProperty('--accent-text', lighten(ACCENT, 0.25));
document.documentElement.style.setProperty('--accent-hover', lighten(ACCENT, 0.08));
}, []);
const navigate = (id) => {
setOpenAsset(null);
setRoute(id);
};
React.useEffect(() => {
window.ZAMPP_API.loadData()
.then(() => setDataReady(true))
.catch(err => { console.error('[Dragonflight] load failed:', err); setDataReady(true); });
}, []);
const navigate = (id) => { setOpenAsset(null); setRoute(id); };
const crumbs = React.useMemo(() => {
if (openAsset) return [
{ label: "Library", to: "library" },
{ label: openAsset.project, to: "library" },
{ label: 'Library', to: 'library' },
{ label: openAsset.project || 'Library', to: 'library' },
{ label: openAsset.name },
];
if (openProject) return [
{ label: "Projects", to: "projects" },
{ label: 'Projects', to: 'projects' },
{ label: openProject.name },
];
const labels = {
home: ["Home"],
library: ["Library", "Protour 2026"],
projects: ["Projects"],
upload: ["Ingest", "Upload"],
recorders: ["Ingest", "Recorders"],
capture: ["Ingest", "Capture"],
monitors: ["Ingest", "Monitors"],
jobs: ["Jobs"],
editor: ["Editor"],
users: ["Admin", "Users & Groups"],
tokens: ["Admin", "Tokens"],
containers: ["Admin", "Containers"],
cluster: ["Admin", "Cluster"],
settings: ["Admin", "Settings"],
home: ['Home'], library: ['Library'], projects: ['Projects'],
upload: ['Ingest', 'Upload'], recorders: ['Ingest', 'Recorders'],
capture: ['Ingest', 'Capture'], monitors: ['Ingest', 'Monitors'],
jobs: ['Jobs'], editor: ['Editor'],
users: ['Admin', 'Users & Groups'], tokens: ['Admin', 'Tokens'],
containers: ['Admin', 'Containers'], cluster: ['Admin', 'Cluster'],
settings: ['Admin', 'Settings'],
};
return (labels[route] || ["Home"]).map((label, i, arr) => i < arr.length - 1 ? { label } : { label });
return (labels[route] || ['Home']).map(label => ({ label }));
}, [route, openAsset, openProject]);
if (!dataReady) {
return (
<>
<style>{'@keyframes _df_spin{to{transform:rotate(360deg)}}'}</style>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100vh', flexDirection: 'column', gap: 14, background: 'var(--bg-0)' }}>
<div style={{ width: 28, height: 28, borderRadius: '50%', border: '2px solid var(--border)', borderTopColor: ACCENT, animation: '_df_spin 0.8s linear infinite' }} />
<div style={{ fontSize: 12, color: 'var(--text-3)', fontFamily: 'var(--font-mono)' }}>Loading Dragonflight</div>
</div>
</>
);
}
let content;
if (openAsset) {
content = <AssetDetail asset={openAsset} onClose={() => setOpenAsset(null)} />;
} else {
switch (route) {
case "home": content = <Home navigate={navigate} />; break;
case "library": content = <Library navigate={navigate} onOpenAsset={setOpenAsset} project={openProject?.name || "Protour 2026"} />; break;
case "projects": content = <Projects navigate={navigate} onOpenProject={(p) => { setOpenProject(p); setRoute("library"); }} />; break;
case "upload": content = <Upload navigate={navigate} />; break;
case "recorders": content = <Recorders navigate={navigate} onNew={() => setShowNewRecorder(true)} />; break;
case "capture": content = <Capture navigate={navigate} />; break;
case "monitors": content = <Monitors navigate={navigate} />; break;
case "jobs": content = <Jobs navigate={navigate} />; break;
case "editor": content = <Editor />; break;
case "users": content = <Users />; break;
case "tokens": content = <Tokens />; break;
case "containers": content = <Containers />; break;
case "cluster": content = <Cluster />; break;
case "settings": content = <Settings />; break;
default: content = <Home navigate={navigate} />;
case 'home': content = <Home navigate={navigate} />; break;
case 'library': content = <Library navigate={navigate} onOpenAsset={setOpenAsset} openProject={openProject} />; break;
case 'projects': content = <Projects navigate={navigate} onOpenProject={(p) => { setOpenProject(p); setRoute('library'); }} />; break;
case 'upload': content = <Upload navigate={navigate} />; break;
case 'recorders': content = <Recorders navigate={navigate} onNew={() => setShowNewRecorder(true)} />; break;
case 'capture': content = <Capture navigate={navigate} />; break;
case 'monitors': content = <Monitors navigate={navigate} />; break;
case 'jobs': content = <Jobs navigate={navigate} />; break;
case 'editor': content = <Editor />; break;
case 'users': content = <Users />; break;
case 'tokens': content = <Tokens />; break;
case 'containers':content = <Containers />; break;
case 'cluster': content = <Cluster />; break;
case 'settings': content = <Settings />; break;
default: content = <Home navigate={navigate} />;
}
}
return (
<div className="app" data-density={t.density} data-grid-size={t.gridSize} data-sidebar={t.sidebarMode}>
<Sidebar active={openAsset ? "library" : route} onNavigate={navigate} />
<div className="app" data-density="comfortable" data-grid-size="md" data-sidebar="expanded">
<Sidebar active={openAsset ? 'library' : route} onNavigate={navigate} />
<div className="main">
{!openAsset && <Topbar crumbs={crumbs} onNavigate={navigate} />}
{content}
</div>
{showNewRecorder && <NewRecorderModal open={showNewRecorder} onClose={() => setShowNewRecorder(false)} />}
{window.TweaksPanel && (
<window.TweaksPanel title="Tweaks">
<window.TweakSection label="Theme">
<window.TweakColor
label="Accent"
value={t.accent}
onChange={(v) => setTweak("accent", v)}
options={["#5B7CFA", "#7C5CFF", "#2DD4A8", "#FF5B5B", "#F5A623", "#E8E8E8"]}
/>
</window.TweakSection>
<window.TweakSection label="Density">
<window.TweakRadio
label="Density"
value={t.density}
onChange={(v) => setTweak("density", v)}
options={[{ value: "comfortable", label: "Comfy" }, { value: "compact", label: "Compact" }]}
/>
<window.TweakRadio
label="Grid size"
value={t.gridSize}
onChange={(v) => setTweak("gridSize", v)}
options={[{ value: "sm", label: "S" }, { value: "md", label: "M" }, { value: "lg", label: "L" }]}
/>
</window.TweakSection>
<window.TweakSection label="Navigation">
<window.TweakRadio
label="Sidebar"
value={t.sidebarMode}
onChange={(v) => setTweak("sidebarMode", v)}
options={[{ value: "expanded", label: "Expanded" }, { value: "collapsed", label: "Icons" }]}
/>
</window.TweakSection>
<window.TweakSection label="Quick jump">
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 6 }}>
{[
["home", "Home"], ["library", "Library"], ["recorders", "Recorders"],
["capture", "Capture"], ["monitors", "Monitors"], ["jobs", "Jobs"],
["editor", "Editor"], ["cluster", "Cluster"],
].map(([k, l]) => (
<button key={k} className="btn ghost sm" style={{ justifyContent: "flex-start", border: "1px solid var(--border)" }} onClick={() => navigate(k)}>
{l}
</button>
))}
<button className="btn ghost sm" style={{ justifyContent: "flex-start", border: "1px solid var(--border)", gridColumn: "1 / -1" }} onClick={() => { navigate("library"); setTimeout(() => setOpenAsset(window.ZAMPP_DATA.ASSETS[1]), 50); }}>
Open asset detail
</button>
<button className="btn ghost sm" style={{ justifyContent: "flex-start", border: "1px solid var(--border)", gridColumn: "1 / -1" }} onClick={() => { navigate("recorders"); setTimeout(() => setShowNewRecorder(true), 50); }}>
Open "New recorder" modal
</button>
</div>
</window.TweakSection>
</window.TweaksPanel>
)}
</div>
);
}
function hexToRgba(hex, a) {
const h = hex.replace("#", "");
const h = hex.replace('#', '');
const r = parseInt(h.slice(0, 2), 16);
const g = parseInt(h.slice(2, 4), 16);
const b = parseInt(h.slice(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${a})`;
return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
}
function lighten(hex, amt) {
const h = hex.replace("#", "");
const h = hex.replace('#', '');
const r = Math.min(255, parseInt(h.slice(0, 2), 16) + Math.round(amt * 255));
const g = Math.min(255, parseInt(h.slice(2, 4), 16) + Math.round(amt * 255));
const b = Math.min(255, parseInt(h.slice(4, 6), 16) + Math.round(amt * 255));
return `rgb(${r}, ${g}, ${b})`;
return 'rgb(' + r + ',' + g + ',' + b + ')';
}
const root = ReactDOM.createRoot(document.getElementById("root"));
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);