booth + mansion: fix loading-state flashes (0.57.0)
- BoothDashboard had no loaded flag, so returning publishers briefly saw the
'Welcome to your booth' new-user onboarding before their classes loaded
- add loaded-gating + a shimmer skeleton; welcome shows only after load
- same fix for the Mansion 'My showcases' list ('No showcases yet' flash)
- completes the loading-skeleton pattern across all data surfaces
(Browse, Booth, Mansion)
- verified live (throttled): skeleton during load, no welcome flash,
welcome/content only after loaded
This commit is contained in:
parent
04128a24d3
commit
fb3c523d4a
@ -4,7 +4,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#0e0a14" />
|
<meta name="theme-color" content="#0e0a14" />
|
||||||
<meta name="description" content="amerc is the agent mercenary tavern — hire, run, publish, and remotely deliver AI agents across software boundaries. No infrastructure, just skills." />
|
<meta name="description" content="amerc is the agent mercenary tavern — hire, run, publish, and remotely deliver AI agents across software boundaries. No infrastructure, just skills." />
|
||||||
<meta name="version" content="0.56.0-routes" />
|
<meta name="version" content="0.57.0-loadstates" />
|
||||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||||
<link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" />
|
<link rel="icon" href="/app-192.png" type="image/png" sizes="192x192" />
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "amerc-site",
|
"name": "amerc-site",
|
||||||
"version": "0.56.0-routes",
|
"version": "0.57.0-loadstates",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -223,6 +223,7 @@ export function BoothDashboard({ auth, setRoute }) {
|
|||||||
const [err, setErr] = useState('');
|
const [err, setErr] = useState('');
|
||||||
const [form, setForm] = useState({ name: '', tags: '', description: '', kind: 'codex', command: 'codex --yolo', visibility: 'public' });
|
const [form, setForm] = useState({ name: '', tags: '', description: '', kind: 'codex', command: 'codex --yolo', visibility: 'public' });
|
||||||
const [newInst, setNewInst] = useState(null);
|
const [newInst, setNewInst] = useState(null);
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setErr('');
|
setErr('');
|
||||||
@ -231,7 +232,7 @@ export function BoothDashboard({ auth, setRoute }) {
|
|||||||
setClasses(c.classes || []);
|
setClasses(c.classes || []);
|
||||||
const all = await Promise.all((c.classes || []).map((x) => api(`/agents/instances?classId=${x.id}`).then((r) => r.instances).catch(() => [])));
|
const all = await Promise.all((c.classes || []).map((x) => api(`/agents/instances?classId=${x.id}`).then((r) => r.instances).catch(() => [])));
|
||||||
setInsts(all.flat());
|
setInsts(all.flat());
|
||||||
} catch (e) { setErr(e.message); }
|
} catch (e) { setErr(e.message); } finally { setLoaded(true); }
|
||||||
}, []);
|
}, []);
|
||||||
useEffect(() => { if (auth.user) load(); }, [auth.user, load]);
|
useEffect(() => { if (auth.user) load(); }, [auth.user, load]);
|
||||||
|
|
||||||
@ -269,7 +270,15 @@ export function BoothDashboard({ auth, setRoute }) {
|
|||||||
<button className="px-action px-auth-submit" type="submit">Publish class</button>
|
<button className="px-action px-auth-submit" type="submit">Publish class</button>
|
||||||
</form>
|
</form>
|
||||||
<div className="ap-mine">
|
<div className="ap-mine">
|
||||||
{!classes.length ? (
|
{!loaded ? (
|
||||||
|
<div className="ap-mine-skel" aria-hidden="true">
|
||||||
|
<span className="sk-p" style={{ width: '40%', height: 12 }} />
|
||||||
|
<div className="ap-skel-row"><span className="sk-av" /><span className="sk-p" style={{ flex: 1 }} /></div>
|
||||||
|
<div className="ap-skel-row"><span className="sk-av" /><span className="sk-p" style={{ flex: 1 }} /></div>
|
||||||
|
<span className="sk-p" style={{ width: '30%', height: 12, marginTop: 14 }} />
|
||||||
|
<div className="ap-skel-row"><span className="sk-p" style={{ flex: 1 }} /></div>
|
||||||
|
</div>
|
||||||
|
) : !classes.length ? (
|
||||||
<div className="ap-booth-welcome">
|
<div className="ap-booth-welcome">
|
||||||
<span className="ap-booth-welcome-ico" aria-hidden="true">🗡️</span>
|
<span className="ap-booth-welcome-ico" aria-hidden="true">🗡️</span>
|
||||||
<h3>Welcome to your booth, {auth.user.handle}.</h3>
|
<h3>Welcome to your booth, {auth.user.handle}.</h3>
|
||||||
|
|||||||
@ -68,9 +68,10 @@ function ShowcaseRoom({ loggedIn, handle, setRoute }) {
|
|||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [created, setCreated] = useState(null);
|
const [created, setCreated] = useState(null);
|
||||||
const [err, setErr] = useState('');
|
const [err, setErr] = useState('');
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
if (!loggedIn) return;
|
if (!loggedIn) return;
|
||||||
try { const d = await api('/showcase'); setList(d.showcases || []); } catch (e) { setErr(e.message); }
|
try { const d = await api('/showcase'); setList(d.showcases || []); } catch (e) { setErr(e.message); } finally { setLoaded(true); }
|
||||||
}, [loggedIn]);
|
}, [loggedIn]);
|
||||||
useEffect(() => { load(); }, [load]);
|
useEffect(() => { load(); }, [load]);
|
||||||
const register = async (e) => {
|
const register = async (e) => {
|
||||||
@ -112,14 +113,23 @@ function ShowcaseRoom({ loggedIn, handle, setRoute }) {
|
|||||||
)}
|
)}
|
||||||
<div className="mn-mine">
|
<div className="mn-mine">
|
||||||
<h4 className="ap-h4">My showcases</h4>
|
<h4 className="ap-h4">My showcases</h4>
|
||||||
{list.map((s) => (
|
{!loaded ? (
|
||||||
<div key={s.id} className="mn-row">
|
<div className="mn-skel" aria-hidden="true">
|
||||||
<a href={`https://${s.host}/`} target="_blank" rel="noreferrer">{s.host}</a>
|
<div className="ap-skel-row"><span className="sk-p" style={{ flex: 1 }} /></div>
|
||||||
<span>registered {new Date(s.created_at).toLocaleDateString()}</span>
|
<div className="ap-skel-row"><span className="sk-p" style={{ flex: 1 }} /></div>
|
||||||
<button className="px-action" onClick={() => del(s.id)}>delete</button>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
) : (
|
||||||
{!list.length && <p className="ap-hint">No showcases yet — register one above.</p>}
|
<>
|
||||||
|
{list.map((s) => (
|
||||||
|
<div key={s.id} className="mn-row">
|
||||||
|
<a href={`https://${s.host}/`} target="_blank" rel="noreferrer">{s.host}</a>
|
||||||
|
<span>registered {new Date(s.created_at).toLocaleDateString()}</span>
|
||||||
|
<button className="px-action" onClick={() => del(s.id)}>delete</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{!list.length && <p className="ap-hint">No showcases yet — register one above.</p>}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import Account from './Account.jsx';
|
|||||||
import TavernGame from './TavernGame.jsx';
|
import TavernGame from './TavernGame.jsx';
|
||||||
import { api } from './api.js';
|
import { api } from './api.js';
|
||||||
|
|
||||||
const RELEASE = '0.56.0-routes';
|
const RELEASE = '0.57.0-loadstates';
|
||||||
|
|
||||||
const NAV = [
|
const NAV = [
|
||||||
{ id: 'home', label: 'Tavern' },
|
{ id: 'home', label: 'Tavern' },
|
||||||
|
|||||||
@ -1407,3 +1407,9 @@ button {
|
|||||||
.route-enter { flex: 1; min-height: 0; display: flex; flex-direction: column; animation: route-in .34s cubic-bezier(.2,.7,.2,1); }
|
.route-enter { flex: 1; min-height: 0; display: flex; flex-direction: column; animation: route-in .34s cubic-bezier(.2,.7,.2,1); }
|
||||||
@keyframes route-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
|
@keyframes route-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
|
||||||
@media (prefers-reduced-motion: reduce) { .route-enter { animation: none; } }
|
@media (prefers-reduced-motion: reduce) { .route-enter { animation: none; } }
|
||||||
|
|
||||||
|
/* booth + mansion loading skeletons (0.57) */
|
||||||
|
.ap-mine-skel { display: flex; flex-direction: column; gap: 12px; }
|
||||||
|
.ap-skel-row { display: flex; align-items: center; gap: 11px; }
|
||||||
|
.mn-skel { display: flex; flex-direction: column; gap: 10px; margin-top: 8px; }
|
||||||
|
.mn-skel .ap-skel-row .sk-p { height: 12px; }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user