feat(ui): wire library, jobs, ingest, editor screens to live API data: screens-library.jsx

This commit is contained in:
Zac Gaetano 2026-05-22 10:05:54 -04:00
parent 69f0d130ee
commit bc03ee866b

View file

@ -1,57 +1,69 @@
// screens-library.jsx library / asset grid + asset detail // screens-library.jsx
const { ASSETS: ALL_ASSETS, BINS, COMMENTS, PROJECTS } = window.ZAMPP_DATA;
function Library({ navigate, onOpenAsset, project = "Protour 2026" }) { function Library({ navigate, onOpenAsset, openProject }) {
const [bin, setBin] = React.useState("b1"); const { ASSETS: ALL_ASSETS, BINS, PROJECTS } = window.ZAMPP_DATA;
const [view, setView] = React.useState("grid"); const [view, setView] = React.useState('grid');
const [filter, setFilter] = React.useState("all"); const [filter, setFilter] = React.useState('all');
const [search, setSearch] = React.useState(""); const [search, setSearch] = React.useState('');
let assets = ALL_ASSETS; let assets = openProject
if (filter !== "all") assets = assets.filter(a => a.status === filter); ? ALL_ASSETS.filter(a => a.project_id === openProject.id)
: ALL_ASSETS;
if (filter !== 'all') assets = assets.filter(a => a.status === filter);
if (search) assets = assets.filter(a => a.name.toLowerCase().includes(search.toLowerCase())); if (search) assets = assets.filter(a => a.name.toLowerCase().includes(search.toLowerCase()));
const displayTitle = openProject ? openProject.name : 'All Assets';
const errorCount = ALL_ASSETS.filter(a => a.status === 'error').length;
const recentCount = ALL_ASSETS.filter(a => (Date.now() - new Date(a.created_at)) < 86400000).length;
return ( return (
<div className="library-layout"> <div className="library-layout">
<aside className="library-rail"> <aside className="library-rail">
<div> <div>
<h4>Projects</h4> <h4>Projects</h4>
<div className="rail-list"> <div className="rail-list">
{PROJECTS.slice(0, 6).map(p => ( <div className={`rail-item ${!openProject ? 'active' : ''}`} onClick={() => navigate('library')} style={{ cursor: 'pointer' }}>
<div key={p.id} className={`rail-item ${p.name === project ? "active" : ""}`}> <Icon name="library" size={13} className="rail-icon" />
<span>All projects</span>
<span className="rail-count">{ALL_ASSETS.length}</span>
</div>
{PROJECTS.slice(0, 8).map(p => (
<div key={p.id} className={`rail-item ${openProject && openProject.id === p.id ? 'active' : ''}`} style={{ cursor: 'pointer' }}
onClick={() => { navigate('projects'); }}>
<span className="rail-color-dot" style={{ background: p.color }} /> <span className="rail-color-dot" style={{ background: p.color }} />
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{p.name}</span> <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.name}</span>
<span className="rail-count">{p.assets}</span> <span className="rail-count">{p.assets}</span>
</div> </div>
))} ))}
</div> </div>
</div> </div>
<div> {BINS.length > 0 && (
<h4>Bins</h4> <div>
<div className="rail-list"> <h4>Bins</h4>
{BINS.map(b => ( <div className="rail-list">
<div key={b.id} className={`rail-item ${bin === b.id ? "active" : ""}`} onClick={() => setBin(b.id)}> {BINS.map(b => (
<Icon name={binIcon(b.icon)} size={13} className="rail-icon" /> <div key={b.id} className="rail-item">
<span>{b.name}</span> <Icon name={binIcon(b.icon)} size={13} className="rail-icon" />
<span className="rail-count">{b.count}</span> <span>{b.name}</span>
</div> <span className="rail-count">{b.count}</span>
))} </div>
))}
</div>
</div> </div>
</div> )}
<div> <div>
<h4>Smart filters</h4> <h4>Smart filters</h4>
<div className="rail-list"> <div className="rail-list">
<div className="rail-item"><Icon name="alert" size={13} className="rail-icon" /><span>Errors</span><span className="rail-count">2</span></div> {errorCount > 0 && <div className="rail-item" onClick={() => setFilter('error')} style={{ cursor: 'pointer' }}><Icon name="alert" size={13} className="rail-icon" /><span>Errors</span><span className="rail-count">{errorCount}</span></div>}
<div className="rail-item"><Icon name="clock" size={13} className="rail-icon" /><span>Last 24h</span><span className="rail-count">38</span></div> <div className="rail-item" onClick={() => setFilter('all')} style={{ cursor: 'pointer' }}><Icon name="clock" size={13} className="rail-icon" /><span>Last 24h</span><span className="rail-count">{recentCount}</span></div>
<div className="rail-item"><Icon name="comment" size={13} className="rail-icon" /><span>With comments</span><span className="rail-count">9</span></div> <div className="rail-item" onClick={() => setFilter('ready')} style={{ cursor: 'pointer' }}><Icon name="check" size={13} className="rail-icon" /><span>Ready</span><span className="rail-count">{ALL_ASSETS.filter(a => a.status === 'ready').length}</span></div>
<div className="rail-item"><Icon name="check" size={13} className="rail-icon" /><span>Approved</span><span className="rail-count">14</span></div>
</div> </div>
</div> </div>
</aside> </aside>
<div className="library-main"> <div className="library-main">
<div className="library-toolbar"> <div className="library-toolbar">
<div className="toolbar-title">{project}</div> <div className="toolbar-title">{displayTitle}</div>
<span className="count">· {assets.length} assets</span> <span className="count">· {assets.length} assets</span>
<div style={{ flex: 1 }} /> <div style={{ flex: 1 }} />
<div className="search" style={{ width: 220 }}> <div className="search" style={{ width: 220 }}>
@ -59,49 +71,43 @@ function Library({ navigate, onOpenAsset, project = "Protour 2026" }) {
<input value={search} onChange={e => setSearch(e.target.value)} placeholder="Filter assets…" /> <input value={search} onChange={e => setSearch(e.target.value)} placeholder="Filter assets…" />
</div> </div>
<div className="tab-group"> <div className="tab-group">
{["all", "ready", "processing", "live", "error"].map(f => ( {['all', 'ready', 'processing', 'live', 'error'].map(f => (
<button key={f} className={filter === f ? "active" : ""} onClick={() => setFilter(f)}> <button key={f} className={filter === f ? 'active' : ''} onClick={() => setFilter(f)}>
{f === "all" ? "All" : f[0].toUpperCase() + f.slice(1)} {f === 'all' ? 'All' : f[0].toUpperCase() + f.slice(1)}
</button> </button>
))} ))}
</div> </div>
<div className="tab-group"> <div className="tab-group">
<button className={view === "grid" ? "active" : ""} onClick={() => setView("grid")} data-tip="Grid"><Icon name="grid" size={12} /></button> <button className={view === 'grid' ? 'active' : ''} onClick={() => setView('grid')}><Icon name="grid" size={12} /></button>
<button className={view === "list" ? "active" : ""} onClick={() => setView("list")} data-tip="List"><Icon name="list" size={12} /></button> <button className={view === 'list' ? 'active' : ''} onClick={() => setView('list')}><Icon name="list" size={12} /></button>
</div> </div>
<button className="btn primary"><Icon name="upload" />Upload</button> <button className="btn primary"><Icon name="upload" />Upload</button>
</div> </div>
{view === "grid" ? ( {assets.length === 0 ? (
<div style={{ padding: 60, textAlign: 'center', color: 'var(--text-3)' }}>No assets match this filter.</div>
) : view === 'grid' ? (
<div className="library-grid"> <div className="library-grid">
{assets.map(a => <AssetCard key={a.id} asset={a} onOpen={() => onOpenAsset(a)} />)} {assets.map(a => <AssetCard key={a.id} asset={a} onOpen={() => onOpenAsset(a)} />)}
</div> </div>
) : ( ) : (
<div className="library-list"> <div className="library-list">
<div className="list-row head"> <div className="list-row head">
<div></div> <div></div><div>Name</div><div>Duration</div><div>Resolution</div><div>Codec</div><div>Size</div><div>Updated</div><div></div>
<div>Name</div>
<div>Duration</div>
<div>Resolution</div>
<div>Codec</div>
<div>Size</div>
<div>Updated</div>
<div></div>
</div> </div>
{assets.map(a => ( {assets.map(a => (
<div key={a.id} className="list-row" onClick={() => onOpenAsset(a)}> <div key={a.id} className="list-row" onClick={() => onOpenAsset(a)} style={{ cursor: 'pointer' }}>
<div className="thumb"><AssetThumb asset={a} /></div> <div className="thumb"><AssetThumb asset={a} /></div>
<div> <div>
<div className="name">{a.name}</div> <div className="name">{a.name}</div>
<div style={{ display: "flex", gap: 6, marginTop: 2 }}> <div style={{ display: 'flex', gap: 6, marginTop: 2 }}>
<StatusDot status={a.status} /> <StatusDot status={a.status} />
<span style={{ fontSize: 11, color: "var(--text-3)" }}>{a.status}</span> <span style={{ fontSize: 11, color: 'var(--text-3)' }}>{a.status}</span>
{a.comments > 0 && <span style={{ fontSize: 11, color: "var(--text-3)" }}>· {a.comments} comments</span>}
</div> </div>
</div> </div>
<div className="col-sub">{a.duration}</div> <div className="col-sub">{a.duration}</div>
<div className="col-sub">{a.res}</div> <div className="col-sub">{a.res}</div>
<div className="col-sub">{a.codec}</div> <div className="col-sub">{a.codec || '—'}</div>
<div className="col-sub">{a.size}</div> <div className="col-sub">{a.size}</div>
<div className="col-sub">{a.updated}</div> <div className="col-sub">{a.updated}</div>
<button className="icon-btn" onClick={e => e.stopPropagation()}><Icon name="more" /></button> <button className="icon-btn" onClick={e => e.stopPropagation()}><Icon name="more" /></button>
@ -119,29 +125,17 @@ function AssetCard({ asset, onOpen }) {
<div className="asset-card" onClick={onOpen}> <div className="asset-card" onClick={onOpen}>
<AssetThumb asset={asset} /> <AssetThumb asset={asset} />
<div className="thumb-status"> <div className="thumb-status">
{asset.status === "live" && <span className="badge live">LIVE</span>} {asset.status === 'live' && <span className="badge live">LIVE</span>}
{asset.status === "processing" && <span className="badge warning">Proxy {asset.progress}%</span>} {asset.status === 'processing' && <span className="badge warning">Processing</span>}
{asset.status === "error" && <span className="badge danger">Error</span>} {asset.status === 'error' && <span className="badge danger">Error</span>}
</div> </div>
{asset.type === "video" && <div className="thumb-duration">{asset.duration}</div>} {(asset.type === 'video' || !asset.type) && asset.duration !== '—' && <div className="thumb-duration">{asset.duration}</div>}
{asset.status === "processing" && (
<div className="asset-progress-overlay">
<div className="asset-progress-bar">
<div className="asset-progress-fill" style={{ width: `${asset.progress || 0}%` }} />
</div>
</div>
)}
<div className="meta"> <div className="meta">
<div className="name">{asset.name}</div> <div className="name">{asset.name}</div>
<div className="sub"> <div className="sub">
<span>{asset.res}</span> <span>{asset.res}</span>
<span>·</span> <span>·</span>
<span>{asset.size}</span> <span>{asset.size}</span>
{asset.comments > 0 && (
<span style={{ marginLeft: "auto", display: "flex", alignItems: "center", gap: 3 }}>
<Icon name="comment" size={10} />{asset.comments}
</span>
)}
</div> </div>
</div> </div>
</div> </div>
@ -149,7 +143,7 @@ function AssetCard({ asset, onOpen }) {
} }
function binIcon(name) { function binIcon(name) {
return { grid: "library", live: "record", film: "film", proxy: "proxy", audio: "audio", package: "package" }[name] || "folder"; return { grid: 'library', live: 'record', film: 'film', proxy: 'proxy', audio: 'audio', package: 'package' }[name] || 'folder';
} }
window.Library = Library; window.Library = Library;