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:
parent
0076e57448
commit
0ecf037b5a
@ -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" />
|
||||||
|
|||||||
@ -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
48
src/Account.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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')} />
|
||||||
|
|||||||
@ -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; } }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user