account/settings page (0.35.0)

- new #/account: profile card (avatar initials + role badge + quick links),
  change-password, and agent keys consolidated in one place
- topbar username now opens Account; separate logout button
- password change moved out of the tavern signed-in panel into a proper settings surface
This commit is contained in:
artheru 2026-06-10 05:39:56 +08:00
parent 0076e57448
commit 0ecf037b5a
6 changed files with 74 additions and 6 deletions

View File

@ -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.34.0-share" /> <meta name="version" content="0.35.0-account" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<!-- Open Graph / social share --> <!-- Open Graph / social share -->
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />

View File

@ -1,6 +1,6 @@
{ {
"name": "amerc-site", "name": "amerc-site",
"version": "0.34.0-share", "version": "0.35.0-account",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {

48
src/Account.jsx Normal file
View File

@ -0,0 +1,48 @@
import React, { useState } from 'react';
import { api } from './api.js';
import { KeysCard } from './AgentPlatform.jsx';
import { toast } from './toast.js';
function avatarStyle(name) { let h = 0; for (const ch of String(name)) h = (h * 31 + ch.charCodeAt(0)) >>> 0; const a = h % 360, b = (a + 70 + h % 90) % 360; return { background: `linear-gradient(135deg, hsl(${a} 68% 46%), hsl(${b} 64% 32%))` }; }
export default function Account({ auth, setRoute }) {
const [cur, setCur] = useState('');
const [nw, setNw] = useState('');
const [err, setErr] = useState('');
if (!auth.ready) return <div className="ap"><p>Loading</p></div>;
if (!auth.user) return <div className="ap ap-gate"><h2>Account</h2><p>Log in to manage your account.</p><button className="px-action" onClick={() => setRoute('signin')}>Go to Login</button></div>;
const u = auth.user;
const changePw = async (e) => {
e.preventDefault(); setErr('');
try { await api('/auth/password', { method: 'POST', body: { currentPassword: cur, newPassword: nw } }); setCur(''); setNw(''); toast('Password updated ✓'); }
catch (e2) { setErr(e2.message); }
};
return (
<div className="ap acct">
<div className="ap-head"><h2>Account</h2></div>
<div className="acct-grid">
<div className="acct-card acct-profile">
<span className="ap-avatar acct-avatar" style={avatarStyle(u.handle)}>{u.handle.slice(0, 2).toUpperCase()}</span>
<h3>{u.handle}</h3>
<p className="acct-email">{u.email}</p>
<span className={`acct-role acct-role-${u.role}`}>{u.role}</span>
<div className="acct-links">
<button className="px-action" onClick={() => setRoute('booth')}>My Booth</button>
<button className="px-action" onClick={() => setRoute('mansion')}>Mansion</button>
<button className="px-action" onClick={() => auth.logout()}>Log out</button>
</div>
</div>
<div className="acct-card">
<h4 className="ap-h4">Change password</h4>
<form className="acct-form" onSubmit={changePw}>
<input type="password" placeholder="current password" value={cur} onChange={(e) => setCur(e.target.value)} required />
<input type="password" placeholder="new password (min 6)" value={nw} onChange={(e) => setNw(e.target.value)} required minLength={6} />
{err && <p className="px-auth-err">{err}</p>}
<button className="px-action px-auth-submit" type="submit">Update password</button>
</form>
</div>
</div>
<KeysCard />
</div>
);
}

View File

@ -165,7 +165,7 @@ export function UsePanel({ instance, onClose }) {
} }
// ---- My Booth: publish classes + instances, manage ------------------------ // ---- My Booth: publish classes + instances, manage ------------------------
function KeysCard() { export function KeysCard() {
const [keys, setKeys] = useState([]); const [keys, setKeys] = useState([]);
const [name, setName] = useState(''); const [name, setName] = useState('');
const [created, setCreated] = useState(null); const [created, setCreated] = useState(null);

View File

@ -3,10 +3,11 @@ import { Menu, X } from 'lucide-react';
import { useAuth, AuthPanel, AdminConsole } from './Auth.jsx'; import { useAuth, AuthPanel, AdminConsole } from './Auth.jsx';
import { AgentBrowse, BoothDashboard } from './AgentPlatform.jsx'; import { AgentBrowse, BoothDashboard } from './AgentPlatform.jsx';
import Mansion from './Mansion.jsx'; import Mansion from './Mansion.jsx';
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.34.0-share'; const RELEASE = '0.35.0-account';
const NAV = [ const NAV = [
{ id: 'home', label: 'Tavern' }, { id: 'home', label: 'Tavern' },
@ -14,7 +15,7 @@ const NAV = [
{ id: 'mansion', label: 'Mansion' }, { id: 'mansion', label: 'Mansion' },
{ id: 'booth', label: 'My Booth' }, { id: 'booth', label: 'My Booth' },
]; ];
const VALID_ROUTES = ['home', 'agents', 'mansion', 'booth', 'admin', 'signin']; const VALID_ROUTES = ['home', 'agents', 'mansion', 'booth', 'account', 'admin', 'signin'];
function useHashRoute() { function useHashRoute() {
const normalize = () => { const normalize = () => {
@ -50,7 +51,10 @@ function PixelTopbar({ route, setRoute, auth }) {
{isAdmin && <a className="px-nav-btn" href="https://pm.amerc.ai/">PM</a>} {isAdmin && <a className="px-nav-btn" href="https://pm.amerc.ai/">PM</a>}
{isAdmin && <button className={`px-nav-btn quartermaster${route === 'admin' ? ' active' : ''}`} onClick={() => go('admin')} type="button">Quartermaster</button>} {isAdmin && <button className={`px-nav-btn quartermaster${route === 'admin' ? ' active' : ''}`} onClick={() => go('admin')} type="button">Quartermaster</button>}
{loggedIn {loggedIn
? <button className="px-nav-btn px-nav-user" onClick={() => auth.logout()} type="button" title="Log out">{auth.user.handle} </button> ? <>
<button className={`px-nav-btn px-nav-user${route === 'account' ? ' active' : ''}`} onClick={() => go('account')} type="button" title="Account">{auth.user.handle}</button>
<button className="px-nav-btn" onClick={() => auth.logout()} type="button" title="Log out"></button>
</>
: <button className={`px-nav-btn${route === 'signin' ? ' active' : ''}`} onClick={() => go('signin')} type="button">Login</button>} : <button className={`px-nav-btn${route === 'signin' ? ' active' : ''}`} onClick={() => go('signin')} type="button">Login</button>}
</> </>
); );
@ -192,6 +196,7 @@ function Scene({ route, setRoute, auth }) {
if (route === 'agents') return <Page setRoute={setRoute}><AgentBrowse auth={auth} setRoute={setRoute} /></Page>; if (route === 'agents') return <Page setRoute={setRoute}><AgentBrowse auth={auth} setRoute={setRoute} /></Page>;
if (route === 'mansion') return <Page setRoute={setRoute}><Mansion auth={auth} setRoute={setRoute} /></Page>; if (route === 'mansion') return <Page setRoute={setRoute}><Mansion auth={auth} setRoute={setRoute} /></Page>;
if (route === 'booth') return <Page setRoute={setRoute}><BoothDashboard auth={auth} setRoute={setRoute} /></Page>; if (route === 'booth') return <Page setRoute={setRoute}><BoothDashboard auth={auth} setRoute={setRoute} /></Page>;
if (route === 'account') return <Page setRoute={setRoute}><Account auth={auth} setRoute={setRoute} /></Page>;
if (route === 'signin') return <Page setRoute={setRoute}><div className="td-signin"><div className="px-board" style={{ position: 'static', transform: 'none', width: 'min(380px,92vw)', margin: '0 auto' }}> if (route === 'signin') return <Page setRoute={setRoute}><div className="td-signin"><div className="px-board" style={{ position: 'static', transform: 'none', width: 'min(380px,92vw)', margin: '0 auto' }}>
<header className="px-board-head"><span className="px-board-tag">GATEHOUSE</span><h2>Open Booth</h2><p>One amerc account across tavern, docs, PM and git.</p></header> <header className="px-board-head"><span className="px-board-tag">GATEHOUSE</span><h2>Open Booth</h2><p>One amerc account across tavern, docs, PM and git.</p></header>
<AuthPanel auth={auth} onDone={() => setRoute('booth')} /> <AuthPanel auth={auth} onDone={() => setRoute('booth')} />

View File

@ -1159,3 +1159,18 @@ button {
.ap-inst-status.pulse { box-shadow: 0 0 0 0 rgba(54,240,176,0.7); animation: ap-pulse 1.8s infinite; } .ap-inst-status.pulse { box-shadow: 0 0 0 0 rgba(54,240,176,0.7); animation: ap-pulse 1.8s infinite; }
.ap-mine-class { display: flex; flex-direction: column; gap: 6px; } .ap-mine-class { display: flex; flex-direction: column; gap: 6px; }
.ap-mine-row .ap-avatar-sm { flex: none; } .ap-mine-row .ap-avatar-sm { flex: none; }
/* account / settings (0.35) */
.acct-grid { display: grid; grid-template-columns: 280px 1fr; gap: 16px; align-items: start; margin-bottom: 6px; }
.acct-card { padding: 20px; border-radius: 14px; background: #130d20; border: 1px solid #271c3a; }
.acct-profile { text-align: center; }
.acct-avatar { width: 64px; height: 64px; border-radius: 16px; font-size: 22px; font-weight: 700; color: #fff; margin: 0 auto 12px; display: flex; align-items: center; justify-content: center; font-family: Georgia, serif; }
.acct-profile h3 { margin: 0; color: #fff; font-size: 18px; }
.acct-email { color: #9a86b8; font-size: 13px; margin: 4px 0 10px; word-break: break-all; }
.acct-role { display: inline-block; font-size: 11px; padding: 3px 11px; border-radius: 12px; text-transform: uppercase; letter-spacing: 1px; }
.acct-role-admin { background: rgba(242,184,95,0.15); color: #f2b85f; border: 1px solid rgba(242,184,95,0.4); }
.acct-role-member { background: rgba(70,200,224,0.12); color: #46c8e0; border: 1px solid rgba(70,200,224,0.3); }
.acct-links { display: flex; flex-direction: column; gap: 8px; margin-top: 18px; }
.acct-form { display: flex; flex-direction: column; gap: 10px; max-width: 360px; }
.acct-form input { padding: 10px; border-radius: 8px; background: #0a0f1a; border: 1px solid #2e2440; color: #eaf2ff; font-family: inherit; }
@media (max-width: 680px) { .acct-grid { grid-template-columns: 1fr; } }