/* global React */ const { useState, useEffect, useRef } = window.ReactHooks; function LoginView({ onLogin }) { const [email, setEmail] = useState("admin@tcviz.com"); const [pw, setPw] = useState("••••••••"); const [err, setErr] = useState(""); return (
TC admin

Sign in

TC Admin · upload, manage and release Power BI visual packages.

{ e.preventDefault(); if (!email || !pw) { setErr("Missing credentials"); return; } onLogin(email); }}>
setEmail(e.target.value)} />
setPw(e.target.value)} />
{err &&
{err}
}
Demo build · any credentials accepted
); } function AdminPanel({ email, onLogout }) { const [section, setSection] = useState("Uploads"); const [files, setFiles] = useState([ { name: "horizon-kpi-2.4.1.pbiviz", size: "182 KB", status: "Published", date: "2026-03-18", v: "2.4.1" }, { name: "strata-waterfall-1.9.0.pbiviz", size: "148 KB", status: "Published", date: "2026-02-11", v: "1.9.0" }, { name: "atlas-small-multiples-3.0.2.pbiviz", size: "221 KB", status: "Published", date: "2026-04-02", v: "3.0.2" }, { name: "meridian-map-4.1.0.pbiviz", size: "398 KB", status: "Pending review", date: "2026-03-30", v: "4.1.0" }, { name: "orbit-network-0.9.2.pbiviz", size: "312 KB", status: "Failed signing", date: "2026-04-12", v: "0.9.2" } ]); const [drag, setDrag] = useState(false); const [uploading, setUploading] = useState(null); const inputRef = useRef(null); const statusClass = (s) => s === "Published" ? "ok" : s === "Pending review" ? "pending" : "err"; const onFiles = (fl) => { const f = fl[0]; if (!f) return; const sizeKB = Math.max(1, Math.round(f.size / 1024)); setUploading({ name: f.name, progress: 0 }); const iv = setInterval(() => { setUploading((u) => { if (!u) return u; const next = Math.min(100, u.progress + 7 + Math.random() * 8); if (next >= 100) { clearInterval(iv); setFiles((list) => [{ name: f.name, size: sizeKB + " KB", status: "Pending review", date: new Date().toISOString().slice(0, 10), v: "—" }, ...list]); setTimeout(() => setUploading(null), 400); return { ...u, progress: 100 }; } return { ...u, progress: next }; }); }, 140); }; return (
{section === "Uploads" && ( <>
Visual packages

Uploads

Destination · Hostinger · /pbiviz/
{ e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)} onDrop={(e) => { e.preventDefault(); setDrag(false); onFiles(e.dataTransfer.files); }} onClick={() => inputRef.current && inputRef.current.click()} >
Drop a .pbiviz package to publish
or click to choose · Files are uploaded to your Hostinger account, signed and queued for review
onFiles(e.target.files)} />
{uploading && (
{uploading.name} {Math.floor(uploading.progress)}%
)} {files.map((f, i) => ( ))}
PackageVersionSizeUploadedStatus
{f.name} {f.v} {f.size} {f.date} {f.status}
)} {section !== "Uploads" && (
Section · {section}
Coming soon

This area of the admin is a placeholder in the current build.

)}
); } function AdminPage() { const [email, setEmail] = useState(() => sessionStorage.getItem("tcviz_admin_email") || ""); useEffect(() => { if (email) sessionStorage.setItem("tcviz_admin_email", email); else sessionStorage.removeItem("tcviz_admin_email"); }, [email]); return email ? setEmail("")} /> : ; } window.AdminPage = AdminPage;