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:
@@ -0,0 +1,57 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api, type User } from './api';
|
||||
import { useI18n } from './i18n/I18nProvider';
|
||||
import { Profile } from './ui/Profile';
|
||||
import { Avatar } from './ui/Avatar';
|
||||
|
||||
// The member's own page: the same form the admin drawer holds, reachable
|
||||
// without the moderation screen. No router — main.tsx picks it by pathname.
|
||||
export function ProfilePage() {
|
||||
const { t } = useI18n();
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [note, setNote] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
api.me()
|
||||
.then((r) => setUser(r.user))
|
||||
.catch(() => undefined)
|
||||
.finally(() => setLoaded(true));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="adm">
|
||||
<header className="adm-bar">
|
||||
<a className="adm-brand" href="/">
|
||||
<img src="/assets/RecipesCamIcon.png" alt="" width="26" height="26" />
|
||||
<span>Recipes<em>Cam</em></span>
|
||||
</a>
|
||||
<b className="adm-title">{t('profile.title')}</b>
|
||||
<span className="adm-spacer" />
|
||||
<Avatar user={user} />
|
||||
<a className="btn ghost" data-key="profile-home" href="/">{t('nav.back')}</a>
|
||||
<button
|
||||
type="button"
|
||||
className="btn ghost"
|
||||
data-key="profile-logout"
|
||||
onClick={async () => {
|
||||
await api.logout().catch(() => undefined);
|
||||
window.location.assign('/');
|
||||
}}
|
||||
>
|
||||
{t('nav.logout')}
|
||||
</button>
|
||||
</header>
|
||||
|
||||
{note ? <p className="hint adm-note">{note}</p> : null}
|
||||
|
||||
{user ? (
|
||||
<Profile onSaved={setNote} onUser={setUser} />
|
||||
) : loaded ? (
|
||||
<p className="adm-note">
|
||||
<a className="btn primary" href="/?auth=login">{t('nav.login')}</a>
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user