feat(ui): wire data.jsx to real API; add loading gate in app.jsx: app.jsx
This commit is contained in:
parent
98025001e8
commit
7dda7cc89c
1 changed files with 62 additions and 113 deletions
|
|
@ -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 />);
|
||||
|
|
|
|||
Loading…
Reference in a new issue