browse: loading skeletons, fix empty-state flash (0.51.0)
- AgentBrowse tracked no loaded state, so on cold load it briefly flashed 'No agents here yet — be the first' before data arrived - add a loaded flag: show 3 shimmer skeleton cards while fetching; gate the empty-state and 'publish your own' card on loaded - skeleton shimmer respects prefers-reduced-motion - also verified (after a stale-Chrome scare) there is NO auth bug: cookieless /api/auth/me returns null, fresh logged-out load shows Login (no admin nav), and the public-agent Use flow works without login - verified live: skeletons during load, no flash, real card+add-card after
This commit is contained in:
parent
c9956fbd3f
commit
c9ad6407c4
@ -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.50.0-hero" />
|
<meta name="version" content="0.51.0-skel" />
|
||||||
<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.50.0-hero",
|
"version": "0.51.0-skel",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -15,6 +15,7 @@ export function AgentBrowse({ auth, setRoute }) {
|
|||||||
const [q, setQ] = useState('');
|
const [q, setQ] = useState('');
|
||||||
const [open, setOpen] = useState(null);
|
const [open, setOpen] = useState(null);
|
||||||
const [err, setErr] = useState('');
|
const [err, setErr] = useState('');
|
||||||
|
const [loaded, setLoaded] = useState(false);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setErr('');
|
setErr('');
|
||||||
@ -22,7 +23,7 @@ export function AgentBrowse({ auth, setRoute }) {
|
|||||||
const [c, s] = await Promise.all([api(`/agents/classes${tag ? `?tag=${encodeURIComponent(tag)}` : ''}`), api('/agents/stats')]);
|
const [c, s] = await Promise.all([api(`/agents/classes${tag ? `?tag=${encodeURIComponent(tag)}` : ''}`), api('/agents/stats')]);
|
||||||
setClasses((c.classes || []).filter((x) => !q || (x.name + x.tags.join() + x.description).toLowerCase().includes(q.toLowerCase())));
|
setClasses((c.classes || []).filter((x) => !q || (x.name + x.tags.join() + x.description).toLowerCase().includes(q.toLowerCase())));
|
||||||
setTags(s.tags || {});
|
setTags(s.tags || {});
|
||||||
} catch (e) { setErr(e.message); }
|
} catch (e) { setErr(e.message); } finally { setLoaded(true); }
|
||||||
}, [tag, q]);
|
}, [tag, q]);
|
||||||
useEffect(() => { load(); }, [load]);
|
useEffect(() => { load(); }, [load]);
|
||||||
|
|
||||||
@ -43,7 +44,14 @@ export function AgentBrowse({ auth, setRoute }) {
|
|||||||
</div>
|
</div>
|
||||||
{err && <p className="px-auth-err">{err}</p>}
|
{err && <p className="px-auth-err">{err}</p>}
|
||||||
<div className="ap-grid">
|
<div className="ap-grid">
|
||||||
{classes.map((c) => (
|
{!loaded && Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<div key={`sk${i}`} className="ap-card ap-skel" aria-hidden="true">
|
||||||
|
<div className="ap-skel-head"><span className="sk-av" /><span className="sk-tt"><i /><i /></span></div>
|
||||||
|
<span className="sk-p" /><span className="sk-p sk-short" />
|
||||||
|
<div className="ap-skel-foot"><i /><i /></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{loaded && classes.map((c) => (
|
||||||
<button key={c.id} className="ap-card" onClick={() => setOpen(c.id)}>
|
<button key={c.id} className="ap-card" onClick={() => setOpen(c.id)}>
|
||||||
<div className="ap-card-head2">
|
<div className="ap-card-head2">
|
||||||
<span className="ap-avatar" style={avatarStyle(c.name)}>{classEmoji(c)}</span>
|
<span className="ap-avatar" style={avatarStyle(c.name)}>{classEmoji(c)}</span>
|
||||||
@ -57,7 +65,7 @@ export function AgentBrowse({ auth, setRoute }) {
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
{classes.length > 0 && (
|
{loaded && classes.length > 0 && (
|
||||||
<button className="ap-card ap-card-add" onClick={() => setRoute('booth')}>
|
<button className="ap-card ap-card-add" onClick={() => setRoute('booth')}>
|
||||||
<span className="ap-add-plus" aria-hidden="true">+</span>
|
<span className="ap-add-plus" aria-hidden="true">+</span>
|
||||||
<strong>Publish your own</strong>
|
<strong>Publish your own</strong>
|
||||||
@ -65,7 +73,7 @@ export function AgentBrowse({ auth, setRoute }) {
|
|||||||
<span className="ap-add-cta">Open My Booth →</span>
|
<span className="ap-add-cta">Open My Booth →</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!classes.length && (
|
{loaded && !classes.length && (
|
||||||
<div className="ap-empty-start">
|
<div className="ap-empty-start">
|
||||||
<h3>No agents here yet — be the first.</h3>
|
<h3>No agents here yet — be the first.</h3>
|
||||||
<ol>
|
<ol>
|
||||||
|
|||||||
@ -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.50.0-hero';
|
const RELEASE = '0.51.0-skel';
|
||||||
|
|
||||||
const NAV = [
|
const NAV = [
|
||||||
{ id: 'home', label: 'Tavern' },
|
{ id: 'home', label: 'Tavern' },
|
||||||
|
|||||||
@ -1356,3 +1356,16 @@ button {
|
|||||||
.gm-cta-primary:hover { transform: translateY(-2px); box-shadow: 0 10px 26px rgba(243,210,122,0.42); }
|
.gm-cta-primary:hover { transform: translateY(-2px); box-shadow: 0 10px 26px rgba(243,210,122,0.42); }
|
||||||
.gm-cta-secondary { background: rgba(18,12,28,0.66); color: #ecdfc6; box-shadow: inset 0 0 0 1.5px rgba(243,210,122,0.5); backdrop-filter: blur(2px); }
|
.gm-cta-secondary { background: rgba(18,12,28,0.66); color: #ecdfc6; box-shadow: inset 0 0 0 1.5px rgba(243,210,122,0.5); backdrop-filter: blur(2px); }
|
||||||
.gm-cta-secondary:hover { background: rgba(42,30,58,0.85); color: #fff; transform: translateY(-2px); }
|
.gm-cta-secondary:hover { background: rgba(42,30,58,0.85); color: #fff; transform: translateY(-2px); }
|
||||||
|
|
||||||
|
/* browse loading skeletons (0.51) */
|
||||||
|
.ap-skel { pointer-events: none; }
|
||||||
|
.ap-skel-head { display: flex; align-items: center; gap: 11px; }
|
||||||
|
.sk-av, .sk-tt i, .sk-p, .ap-skel-foot i { background: linear-gradient(90deg, #1a1330 25%, #261b42 37%, #1a1330 63%); background-size: 400% 100%; animation: sk-shimmer 1.4s ease infinite; border-radius: 6px; display: block; }
|
||||||
|
.sk-av { width: 38px; height: 38px; border-radius: 10px; flex: 0 0 auto; }
|
||||||
|
.sk-tt { display: flex; flex-direction: column; gap: 6px; flex: 1; }
|
||||||
|
.sk-tt i { height: 11px; width: 60%; } .sk-tt i:last-child { width: 35%; height: 9px; }
|
||||||
|
.sk-p { height: 9px; width: 100%; margin-top: 12px; } .sk-p.sk-short { width: 70%; margin-top: 7px; }
|
||||||
|
.ap-skel-foot { display: flex; justify-content: space-between; margin-top: 14px; }
|
||||||
|
.ap-skel-foot i { height: 9px; width: 30%; }
|
||||||
|
@keyframes sk-shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } }
|
||||||
|
@media (prefers-reduced-motion: reduce) { .sk-av, .sk-tt i, .sk-p, .ap-skel-foot i { animation: none; } }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user