2026-10-13 15:34:47 -04:00
// Top-level pages: Home, My work, Inbox, Search, Team, Workspace, Admin, and the user menu.
import { html , useState , useEffect , useRef } from '../vendor/preact-htm.mjs' ;
import { api } from './api.js' ;
import { state , update , useStore , navigate , boardPath , loadUsers , loadTree , toast , setTheme } from './store.js' ;
import { Icon , Avatar , Spinner , Empty , timeAgo , fmtDate , parseDate , ymd , todayStr , textOn , openModal , closePopover , Menu , RichText , useMobile , confirmDialog , promptDialog } from './ui.js' ;
import { NewBoardModal } from './sidebar.js' ;
function PageHead ({ title , sub , children }) {
const mobile = useMobile ();
return html `<header class="page-head">
${ mobile ? html `<button class="icon-btn" onClick= ${ () => update ({ sidebarOpen : true } )}>< ${ Icon } name="menu" size= ${ 20 } /></button>` : null }
<div class="grow"><h1> ${ title } </h1> ${ sub ? html `<p class="muted"> ${ sub } </p>` : null } </div> ${ children } </header>` ;
}
// ─── Home ────────────────────────────────────────────────
export function HomePage () {
const s = useStore ();
const [ work , setWork ] = useState ( null );
useEffect (() => { api . get ( '/api/mywork' ). then ( setWork ). catch (() => setWork ([])); }, []);
const hour = new Date (). getHours ();
const greet = hour < 12 ? 'Good morning' : hour < 18 ? 'Good afternoon' : 'Good evening' ;
const favs = s . boards . filter (( b ) => s . favorites . includes ( b . id ));
const recent = s . boards . slice (). reverse (). slice ( 0 , 8 );
const open = ( work || []). filter (( w ) => ! w . status ? . done );
const overdue = open . filter (( w ) => w . date && w . date . slice ( 0 , 10 ) < todayStr ()). length ;
const today = open . filter (( w ) => w . date && w . date . slice ( 0 , 10 ) === todayStr ()). length ;
const wsName = ( id ) => s . workspaces . find (( w ) => w . id === id ) ? . name ;
return html `<div class="page home">
< ${ PageHead } title= ${ ` ${ greet } , ${ s . me . name . split ( ' ' )[ 0 ] } !` } sub="Quickly access your recent boards, inbox and work." />
<div class="home-grid">
<section class="card home-stats">
<button class="stat" onClick= ${ () => navigate ( '/my-work' ) } ><span class="stat-n"> ${ work ? open . length : '– ' } </span><span class="muted">Open items assigned to you</span></button>
<button class="stat" onClick= ${ () => navigate ( '/my-work' ) } ><span class="stat-n" style= ${ { color : overdue ? 'var(--red)' : undefined } }> ${ work ? overdue : '– ' } </span><span class="muted">Overdue</span></button>
<button class="stat" onClick= ${ () => navigate ( '/my-work' ) } ><span class="stat-n"> ${ work ? today : '– ' } </span><span class="muted">Due today</span></button>
<button class="stat" onClick= ${ () => navigate ( '/inbox' ) } ><span class="stat-n"> ${ s . unread } </span><span class="muted">Unread notifications</span></button>
</section>
${ favs . length ? html `<section><h3 class="section-title">< ${ Icon } name="star" /> Favorites</h3>
<div class="board-cards"> ${ favs . map (( b ) => html `< ${ BoardCard } key= ${ b . id } b= ${ b } ws= ${ wsName ( b . workspace_id ) } />` ) } </div></section>` : null }
<section><h3 class="section-title">Boards</h3>
<div class="board-cards">
${ recent . map (( b ) => html `< ${ BoardCard } key= ${ b . id } b= ${ b } ws= ${ wsName ( b . workspace_id ) } />` ) }
<button class="board-card new" onClick= ${ () => openModal ( NewBoardModal , { workspaceId : s . currentWorkspace || s . workspaces [ 0 ] ? . id } )}>< ${ Icon } name="plus" size= ${ 28 } /><span>New board</span></button>
</div></section>
</div>
</div>` ;
}
function BoardCard ({ b , ws }) {
return html `<button class="board-card" onClick= ${ () => navigate ( boardPath ( b . id )) } >
<div class="bc-preview"><i /><i /><i /><i /></div>
<div class="bc-name">< ${ Icon } name="board" size= ${ 14 } /> ${ b . name } </div>
<div class="muted small"> ${ ws } </div></button>` ;
}
// ─── My work ─────────────────────────────────────────────
export function MyWorkPage () {
const s = useStore ();
const [ items , setItems ] = useState ( null );
const [ showDone , setShowDone ] = useState ( false );
const [ q , setQ ] = useState ( '' );
const [ who , setWho ] = useState ( s . route . q ? . user || s . me . id );
const load = () => api . get ( `/api/mywork ${ who !== s . me . id ? `?user= ${ who } ` : '' } ` ). then ( setItems );
useEffect (() => { setItems ( null ); load (); }, [ who ]);
useEffect (() => { const fn = () => load (); window . addEventListener ( 'wb:board-changed' , fn ); return () => window . removeEventListener ( 'wb:board-changed' , fn ); }, [ who ]);
const t = todayStr ();
const d = new Date (); const endWeek = new Date ( d ); endWeek . setDate ( d . getDate () + ( 7 - d . getDay ()));
const endNext = new Date ( endWeek ); endNext . setDate ( endWeek . getDate () + 7 );
const buckets = [
[ 'Past dates' , ( x ) => x < t , 'var(--red)' ], [ 'Today' , ( x ) => x === t , 'var(--green)' ], [ 'This week' , ( x ) => x > t && x <= ymd ( endWeek ), 'var(--primary)' ],
[ 'Next week' , ( x ) => x > ymd ( endWeek ) && x <= ymd ( endNext ), '#a25ddc' ], [ 'Later' , ( x ) => x > ymd ( endNext ), '#fdab3d' ],
];
const list = ( items || []). filter (( i ) => ( showDone || ! i . status ? . done ) && ( ! q || ( i . name + i . board_name ). toLowerCase (). includes ( q . toLowerCase ())));
const toggleDone = async ( it ) => {
// Flip to the first "done" label (or back to empty).
const data = state . boardData [ it . board_id ] || await api . get ( `/api/boards/ ${ it . board_id } ` );
const col = data . columns . find (( c ) => c . id === it . status_column );
if ( ! col ) return toast ( 'That board has no status column' , 'error' );
const doneLabel = col . settings . labels . find (( l ) => l . done || /^done$/i . test ( l . name ));
if ( ! doneLabel ) return toast ( `" ${ col . title } " has no Done label` , 'error' );
await api . patch ( `/api/items/ ${ it . id } ` , { vals : { [ col . id ] : it . status ? . done ? null : doneLabel . id } });
load ();
};
const Row = ({ it }) => html `<div class="mw-row ${ it . status ? . done ? 'done' : '' } ">
<button class="mw-check ${ it . status ? . done ? 'on' : '' } " onClick= ${ () => toggleDone ( it ) } title="Mark done">< ${ Icon } name="check" size= ${ 12 } /></button>
<button class="mw-name" onClick= ${ () => navigate ( boardPath ( it . board_id , { itemId : it . id } ))}> ${ it . name }${ it . parent_name ? html `<span class="muted small"> · in ${ it . parent_name } </span>` : null } </button>
<span class="mw-board muted small"><span class="dot" style= ${ { background : it . group_color } } /> ${ it . board_name }${ it . group ? ` › ${ it . group } ` : '' } </span>
${ it . status ? html `<span class="mw-status" style= ${ { background : it . status . color , color : textOn ( it . status . color ) } }> ${ it . status . name } </span>` : html `<span class="mw-status empty" />` }
<span class="mw-date ${ it . date && it . date . slice ( 0 , 10 ) < t && ! it . status ? . done ? 'overdue' : '' } "> ${ it . date ? fmtDate ( it . date ) : '' } </span>
</div>` ;
return html `<div class="page mywork">
< ${ PageHead } title="My work" sub="Everything assigned to you, across every board.">
<select class="input sm" value= ${ who } onChange= ${ ( e ) => setWho ( e . target . value ) } title="View someone else's work">
${ s . users . filter (( u ) => u . active ). map (( u ) => html `<option value= ${ u . id } > ${ u . id === s . me . id ? 'Me' : u . name } </option>` ) } </select>
<//>
<div class="toolbar">
<div class="tb-search">< ${ Icon } name="search" /><input placeholder="Search" value= ${ q } onInput= ${ ( e ) => setQ ( e . target . value ) } /></div>
<label class="check-row"><input type="checkbox" checked= ${ showDone } onChange= ${ ( e ) => setShowDone ( e . target . checked ) } /> Show done items</label>
</div>
${ ! items ? html `< ${ Spinner } />` : ! list . length ? html `< ${ Empty } icon="work" title="You're all caught up"><p class="muted">Items show up here when someone assigns you in a People column.</p><//>` : html `
${ buckets . map (([ title , test , color ]) => { const rows = list . filter (( i ) => i . date && test ( i . date . slice ( 0 , 10 ))). sort (( a , b ) => a . date . localeCompare ( b . date ));
return rows . length ? html `<section key= ${ title } class="mw-group"><h3 style= ${ { color } }> ${ title } <span class="muted small"> ${ rows . length } </span></h3> ${ rows . map (( it ) => html `< ${ Row } key= ${ it . id } it= ${ it } />` ) } </section>` : null ; } )}
${ (() => { const rows = list . filter (( i ) => ! i . date ); return rows . length ? html `<section class="mw-group"><h3 style= ${ { color : 'var(--muted)' } }>Without a date <span class="muted small"> ${ rows . length } </span></h3> ${ rows . map (( it ) => html `< ${ Row } key= ${ it . id } it= ${ it } />` ) } </section>` : null ; } )()}
` }
</div>` ;
}
// ─── Inbox ───────────────────────────────────────────────
export function InboxPage () {
const s = useStore ();
const [ rows , setRows ] = useState ( null );
const [ filter , setFilter ] = useState ( 'all' );
const load = () => api . get ( '/api/notifications' ). then ( setRows );
useEffect (() => { load (); const fn = () => load (); window . addEventListener ( 'wb:notifications' , fn ); return () => window . removeEventListener ( 'wb:notifications' , fn ); }, []);
const mark = async ( ids , unread = false ) => { const r = await api . post ( '/api/notifications/read' , { ids , unread }); update ({ unread : r . unread }); load (); };
const markAll = async () => { const r = await api . post ( '/api/notifications/read' , { all : true }); update ({ unread : r . unread }); load (); };
const shown = ( rows || []). filter (( n ) => filter === 'all' || ( filter === 'unread' ? ! n . read : n . kind === filter ));
const icon = { mention : 'message' , update : 'message' , reply : 'reply' , assigned : 'person' , like : 'thumb' , automation : 'bolt' };
return html `<div class="page inbox">
< ${ PageHead } title="Inbox" sub="Mentions, assignments, replies and automation alerts.">
<button class="btn ghost sm" onClick= ${ markAll } disabled= ${ ! s . unread } >Mark all as read</button>
<//>
<div class="row gap wrap"> ${ [[ 'all' , 'All' ], [ 'unread' , 'Unread' ], [ 'mention' , 'Mentions' ], [ 'assigned' , 'Assigned' ], [ 'automation' , 'Automations' ]]. map (([ k , l ]) => html `
<button key= ${ k } class="chip-btn ${ filter === k ? 'on' : '' } " onClick= ${ () => setFilter ( k ) } > ${ l } </button>` ) } </div>
${ ! rows ? html `< ${ Spinner } />` : ! shown . length ? html `< ${ Empty } icon="bell" title="Nothing new"><p class="muted">When someone mentions or assigns you, it shows up here.</p><//>` : html `<div class="notif-list">
${ shown . map (( n ) => html `<div key= ${ n . id } class="notif ${ n . read ? '' : 'unread' } " onClick= ${ () => { if ( ! n . read ) mark ([ n . id ]); if ( n . board_id ) navigate ( boardPath ( n . board_id , { itemId : n . item_id || undefined } )); }}>
<div class="notif-av">< ${ Avatar } user= ${ s . usersById [ n . actor_id ] || ( n . actor_id ? null : { name : '⚡' , email : 'auto' } )} size= ${ 36 } /><span class="notif-kind">< ${ Icon } name= ${ icon [ n . kind ] || 'bell' } size= ${ 11 } /></span></div>
<div class="notif-body"><div>< ${ RichText } text= ${ n . text . replace ( /@\[([^\]]+)\]\([\w-]+\)/g , '@$1' ) } /></div>
<div class="muted small"> ${ n . board_name || '' }${ n . item_name ? ` › ${ n . item_name } ` : '' } · ${ timeAgo ( n . created_at ) } </div></div>
<button class="icon-btn" title= ${ n . read ? 'Mark unread' : 'Mark read' } onClick= ${ ( e ) => { e . stopPropagation (); mark ([ n . id ], !! n . read ); } }><span class="read-dot ${ n . read ? '' : 'on' } " /></button>
</div>` ) } </div>` }
</div>` ;
}
// ─── Search ──────────────────────────────────────────────
export function SearchPage () {
const s = useStore ();
const [ q , setQ ] = useState ( s . route . q ? . q || '' );
const [ res , setRes ] = useState ( null );
const t = useRef ();
useEffect (() => {
clearTimeout ( t . current );
if ( q . trim (). length < 2 ) { setRes ( null ); return ; }
t . current = setTimeout (() => { api . get ( `/api/search?q= ${ encodeURIComponent ( q ) } ` ). then ( setRes ); history . replaceState ({}, '' , `/search?q= ${ encodeURIComponent ( q ) } ` ); }, 200 );
}, [ q ]);
return html `<div class="page search-page">
< ${ PageHead } title="Search everything" />
<div class="big-search">< ${ Icon } name="search" size= ${ 20 } /><input placeholder="Search items, boards and updates" value= ${ q } onInput= ${ ( e ) => setQ ( e . target . value ) } autofocus /></div>
${ res ? html `
${ res . boards . length ? html `<h3 class="section-title">Boards</h3> ${ res . boards . map (( b ) => html `<button key= ${ b . id } class="result" onClick= ${ () => navigate ( boardPath ( b . id )) } >< ${ Icon } name="board" /> ${ b . name } </button>` ) } ` : null }
${ res . items . length ? html `<h3 class="section-title">Items</h3> ${ res . items . map (( i ) => html `<button key= ${ i . id } class="result" onClick= ${ () => navigate ( boardPath ( i . board_id , { itemId : i . id } ))}>
<span class="dot" style= ${ { background : i . group_color || 'var(--muted)' } } /><span class="grow"> ${ i . name } </span><span class="muted small"> ${ i . board_name }${ i . group_title ? ` › ${ i . group_title } ` : '' } </span></button>` ) } ` : null }
${ res . updates . length ? html `<h3 class="section-title">Updates</h3> ${ res . updates . map (( u ) => html `<button key= ${ u . id } class="result update-result" onClick= ${ () => navigate ( boardPath ( u . board_id , { itemId : u . item_id } ))}>
< ${ Avatar } user= ${ s . usersById [ u . user_id ] } size= ${ 24 } /><span class="grow clamp2"> ${ u . body . replace ( /@\[([^\]]+)\]\([\w-]+\)/g , '@$1' ) } </span><span class="muted small"> ${ u . item_name } · ${ timeAgo ( u . created_at ) } </span></button>` ) } ` : null }
${ ! res . boards . length && ! res . items . length && ! res . updates . length ? html `<p class="muted pad">No results for " ${ q } ".</p>` : null }
` : html `<p class="muted pad">Type at least two characters.</p>` }
</div>` ;
}
// ─── Team ────────────────────────────────────────────────
export function PeoplePage () {
const s = useStore ();
const [ q , setQ ] = useState ( '' );
useEffect (() => { loadUsers (); }, []);
const users = s . users . filter (( u ) => ( u . name + u . email ). toLowerCase (). includes ( q . toLowerCase ())). sort (( a , b ) => b . active - a . active );
return html `<div class="page people">
< ${ PageHead } title="Team" sub= ${ ` ${ s . users . filter (( u ) => u . active ). length } people` } >
${ s . me . role === 'admin' ? html `<button class="btn primary sm" onClick= ${ async () => {
const email = await promptDialog ({ title : 'Add a teammate' , label : 'Their company email — they sign in with Google the first time they visit.' , placeholder : 'name@waterschurch.org' , confirm : 'Add' } );
if (email) { try { await api.post('/api/users', { email }); loadUsers(); toast('Added'); } catch (e) { toast(e.message, 'error'); } }
}}>< ${ Icon } name="plus" /> Add person</button>` : null }
<//>
<div class="tb-search wide">< ${ Icon } name="search" /><input placeholder="Search people" value= ${ q } onInput= ${ ( e ) => setQ ( e . target . value ) } /></div>
<div class="people-grid"> ${ users . map (( u ) => html `<div key= ${ u . id } class="person-card ${ u . active ? '' : 'inactive' } ">
< ${ Avatar } user= ${ u } size= ${ 56 } />
<strong> ${ u . name } </strong><span class="muted small"> ${ u . title || ( u . role === 'admin' ? 'Admin' : 'Member' ) } </span>
<a class="muted small" href= ${ 'mailto:' + u . email } > ${ u . email } </a>
<button class="btn ghost sm" onClick= ${ () => navigate ( `/my-work?user= ${ u . id } ` ) } >Assigned work</button>
</div>` ) } </div>
</div>` ;
}
// ─── Workspace overview ──────────────────────────────────
export function WorkspacePage ({ workspaceId }) {
const s = useStore ();
const ws = s . workspaces . find (( w ) => w . id === workspaceId );
if ( ! ws ) return html `< ${ Empty } title="Workspace not found" />` ;
const boards = s . boards . filter (( b ) => b . workspace_id === ws . id );
return html `<div class="page">
< ${ PageHead } title= ${ ws . name } sub= ${ ws . description || ` ${ boards . length } boards` } >
<button class="btn primary sm" onClick= ${ () => openModal ( NewBoardModal , { workspaceId : ws . id } )}>< ${ Icon } name="plus" /> New board</button><//>
<div class="board-cards"> ${ boards . map (( b ) => html `< ${ BoardCard } key= ${ b . id } b= ${ b } ws= ${ s . folders . find (( f ) => f . id === b . folder_id ) ? . name || '' } />` ) } </div>
</div>` ;
}
// ─── Admin ───────────────────────────────────────────────
export function AdminPage () {
const s = useStore ();
const [ tab , setTab ] = useState ( s . route . tab || 'people' );
useEffect (() => { loadUsers (); }, []);
if ( s . me . role !== 'admin' ) return html `< ${ Empty } icon="shield" title="Admins only" />` ;
return html `<div class="page admin">
< ${ PageHead } title="Administration" />
<nav class="panel-tabs">
<button class= ${ tab === 'people' ? 'active' : '' } onClick= ${ () => setTab ( 'people' ) } >People & access</button>
<button class= ${ tab === 'import' ? 'active' : '' } onClick= ${ () => setTab ( 'import' ) } >Import from monday.com</button>
</nav>
${ tab === 'people' ? html `< ${ AdminPeople } />` : html `< ${ MondayImport } />` }
</div>` ;
}
function AdminPeople () {
const s = useStore ();
const patch = async ( u , p ) => { try { await api . patch ( `/api/users/ ${ u . id } ` , p ); loadUsers (); } catch ( e ) { toast ( e . message , 'error' ); } };
return html `<div class="card"><table class="admin-table">
<thead><tr><th>Person</th><th>Email</th><th>Role</th><th>Last seen</th><th>Access</th></tr></thead>
<tbody> ${ s . users . map (( u ) => html `<tr key= ${ u . id } class= ${ u . active ? '' : 'inactive' } >
<td><span class="row gap">< ${ Avatar } user= ${ u } size= ${ 26 } /> ${ u . name } </span></td>
<td class="muted"> ${ u . email } </td>
<td><select class="input sm" value= ${ u . role } disabled= ${ u . id === s . me . id } onChange= ${ ( e ) => patch ( u , { role : e . target . value } )}><option value="member">Member</option><option value="admin">Admin</option></select></td>
<td class="muted small"> ${ u . last_seen ? timeAgo ( u . last_seen ) : 'never signed in' } </td>
<td> ${ u . id === s . me . id ? html `<span class="muted small">you</span>` : html `<button class="btn ghost sm" onClick= ${ async () => {
if ( u . active && ! ( await confirmDialog ({ title : `Remove ${ u . name } 's access?` , body : 'They are signed out everywhere and can no longer sign in. Their items and updates stay.' , confirm : 'Remove access' } ))) return;
patch(u, { active: !u.active });
}}> ${ u . active ? 'Deactivate' : 'Reactivate' } </button>` } </td>
</tr>` ) } </tbody></table>
<p class="muted small pad">Anyone with an allowed company Google account can sign in and becomes a member. Admins can manage people, delete workspaces and run imports.</p>
</div>` ;
}
function MondayImport () {
const [ token , setToken ] = useState ( '' );
const [ boards , setBoards ] = useState ( null );
const [ sel , setSel ] = useState ([]);
const [ status , setStatus ] = useState ( null );
const [ busy , setBusy ] = useState ( false );
const poll = useRef ();
const refresh = () => api . get ( '/api/admin/import/monday' ). then (( st ) => { setStatus ( st ); if ( ! st . running ) { clearInterval ( poll . current ); if ( st . finishedAt ) loadTree (); } });
useEffect (() => { refresh (); return () => clearInterval ( poll . current ); }, []);
const list = async () => {
setBusy ( true );
try { const b = await api . post ( '/api/admin/import/monday/boards' , { token }); setBoards ( b ); setSel ( b . filter (( x ) => ! x . imported ). map (( x ) => x . id )); }
catch ( e ) { toast ( e . message , 'error' ); }
setBusy ( false );
};
const start = async () => {
try { await api . post ( '/api/admin/import/monday' , { token , boardIds : sel }); clearInterval ( poll . current ); poll . current = setInterval ( refresh , 1500 ); refresh (); }
catch ( e ) { toast ( e . message , 'error' ); }
};
return html `<div class="card import-card">
<h3>Bring your boards over from monday.com</h3>
<ol class="steps">
<li>In monday.com, click your avatar → <b>Developers</b> → <b>My access tokens</b> → <b>Show</b>, and copy the token.</li>
<li>Paste it below and load your boards. Pick which ones to bring over.</li>
<li>Everything comes across: workspaces, folders, groups, columns, items, subitems, updates and files. People are matched by email and claim their assignments the first time they sign in.</li>
</ol>
<div class="row gap"><input class="input grow" type="password" placeholder="monday.com API token" value= ${ token } onInput= ${ ( e ) => setToken ( e . target . value ) } autocomplete="off" />
<button class="btn" disabled= ${ ! token || busy } onClick= ${ list } > ${ busy ? 'Loading…' : 'Load boards' } </button></div>
<p class="muted small">The token is only used for this import and is never stored.</p>
${ boards ? html `<div class="import-list">
<label class="check-row"><input type="checkbox" checked= ${ sel . length === boards . filter (( b ) => ! b . imported ). length } onChange= ${ ( e ) => setSel ( e . target . checked ? boards . filter (( b ) => ! b . imported ). map (( b ) => b . id ) : []) } /> <b>All boards</b> ( ${ boards . length } )</label>
${ boards . map (( b ) => html `<label key= ${ b . id } class="check-row ${ b . imported ? 'muted' : '' } ">
<input type="checkbox" disabled= ${ b . imported } checked= ${ sel . includes ( b . id ) } onChange= ${ ( e ) => setSel ( e . target . checked ? [... sel , b . id ] : sel . filter (( x ) => x !== b . id )) } />
${ b . name } <span class="muted small"> ${ b . workspace } · ${ b . items ?? '?' } items ${ b . imported ? ' · already imported' : '' } </span></label>` ) }
</div>
<button class="btn primary" disabled= ${ ! sel . length || status ? . running } onClick= ${ start } >Import ${ sel . length } board ${ sel . length === 1 ? '' : 's' } </button>` : null }
${ status && ( status . running || status . log . length ) ? html `<div class="import-log">
2026-10-15 16:29:29 -04:00
<div class="row gap"><strong> ${ status . running ? 'Importing…' : status . error ? ( status . counts . boards ? 'Import finished with errors' : 'Import failed' ) : 'Import finished' } </strong> ${ status . running ? html `< ${ Spinner } />` : null }
2026-10-13 15:34:47 -04:00
<span class="muted small"> ${ status . counts . boards || 0 } boards · ${ status . counts . items || 0 } items · ${ status . counts . updates || 0 } updates · ${ status . counts . files || 0 } files</span></div>
<pre> ${ status . log . slice ( - 200 ). join ( '\n' ) } </pre></div>` : null }
</div>` ;
}
// ─── User menu ───────────────────────────────────────────
export function UserMenu () {
const s = useStore ();
const signOut = async () => { await api . post ( '/api/auth/logout' ); location . href = '/' ; };
return html `<div class="user-menu">
<div class="um-head">< ${ Avatar } user= ${ s . me } size= ${ 40 } /><div><strong> ${ s . me . name } </strong><div class="muted small"> ${ s . me . email } </div></div></div>
< ${ Menu } items= ${ [
{ label : 'Edit my name' , icon : 'edit' , onClick : async () => { const n = await promptDialog ({ title : 'Your name' , value : s . me . name } ); if (n) { const me = await api.patch('/api/me', { name: n }); update({ me }); loadUsers(); } } },
{ label: 'My work', icon: 'work', onClick: () => navigate('/my-work') },
{ divider: true },
{ label: 'Light theme', icon: 'sun', active: s.theme === 'light', onClick: () => setTheme('light') },
{ label: 'Dark theme', icon: 'moon', active: s.theme === 'dark', onClick: () => setTheme('dark') },
{ label: 'Night theme', icon: 'moon', active: s.theme === 'night', onClick: () => setTheme('night') },
{ label: 'Match system', icon: 'gear', active: s.theme === 'system', onClick: () => setTheme('system') },
{ divider: true },
s.me.role === 'admin' && { label: 'Administration', icon: 'shield', onClick: () => navigate('/admin') },
{ label: 'Sign out', icon: 'logout', onClick: signOut },
]} />
</div>` ;
}