web: account avatars, member /profile, framed admin panel

- an account can carry a picture: POST /api/auth/avatar (raw bytes,
  sniffed, replaces and unlinks the old file) and the public
  GET /api/users/:id/avatar. It rides wherever the account is named —
  the landing chip, the studio TopBar, the profile form.
- new /profile page for members, sharing one Profile form (picture,
  email, password) with the admin drawer.
- /admin is now one bordered frame whose left column is
  Profile / User account / Pictures / Close. Pictures lists every
  photo in the system with the slot that shows it; User account lists
  each account's name, email, picture and contribution count.
- account control opens a menu: Admin page + Log out for an admin,
  Profile + Log out for a member.
This commit is contained in:
2026-09-18 08:19:36 +07:00
parent 2917c034ed
commit 6bbf77860b
15 changed files with 908 additions and 79 deletions
+22 -4
View File
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react';
import { useI18n, LANGS } from '../i18n/I18nProvider';
import { ACCENTS, FONT_SETS, useTheme } from '../theme/ThemeProvider';
import { Avatar } from './Avatar';
import type { User } from '../api';
// Header: the app's TopBar minus everything camera-only, plus the two controls
@@ -123,18 +124,35 @@ export function TopBar({
</div>
<div className="menu-wrap">
<button type="button" className="btn ghost" onClick={() => setMenu(menu === 'account' ? null : 'account')}>
{user ? user.email.split('@')[0] : t('nav.guest')}
<button
type="button"
className="btn ghost account-btn"
data-key="nav-account"
aria-expanded={menu === 'account'}
onClick={() => setMenu(menu === 'account' ? null : 'account')}
>
{user ? (
<>
<Avatar user={user} small />
{user.email.split('@')[0]}
</>
) : (
t('nav.guest')
)}
</button>
{menu === 'account' ? (
<div className="popover">
{user ? (
<>
<p className="hint">{t('auth.loggedInAs', { email: user.email })}</p>
{/* The account control opens the one page that account can act
on: moderation for an admin, its own profile for a member. */}
{user.admin ? (
<a className="btn" data-key="nav-admin" href="/admin">{t('nav.admin')}</a>
) : null}
<button type="button" className="btn" onClick={onLogout}>{t('nav.logout')}</button>
) : (
<a className="btn" data-key="nav-profile" href="/profile">{t('nav.profile')}</a>
)}
<button type="button" className="btn" data-key="nav-logout" onClick={onLogout}>{t('nav.logout')}</button>
</>
) : (
<>