Files
RecipesCam/docker/frontend/src/ui/Profile.tsx
T
3dtours 6bbf77860b 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.
2026-09-18 08:19:36 +07:00

131 lines
3.9 KiB
TypeScript

import { useEffect, useRef, useState } from 'react';
import { api, type User } from '../api';
import { useI18n } from '../i18n/I18nProvider';
import { Avatar } from './Avatar';
// The signed-in account editing itself: picture, email, password. The API
// re-verifies the current password and the login limiter caps guessing, so this
// form holds no secret. Shared by the /admin settings drawer and /profile.
export function Profile({ onSaved, onUser }: { onSaved: (note: string) => void; onUser?: (user: User) => void }) {
const { t } = useI18n();
const [user, setUser] = useState<User | null>(null);
const [email, setEmail] = useState('');
const [current, setCurrent] = useState('');
const [password, setPassword] = useState('');
const [busy, setBusy] = useState(false);
const [err, setErr] = useState<string | null>(null);
const filePick = useRef<HTMLInputElement>(null);
useEffect(() => {
api.me().then((r) => {
setUser(r.user);
setEmail(r.user?.email ?? '');
}).catch(() => undefined);
}, []);
const keep = (u: User) => {
setUser(u);
setEmail(u.email);
onUser?.(u);
};
const pick = async (file: File) => {
setBusy(true);
setErr(null);
try {
keep((await api.uploadAvatar(file)).user);
onSaved(t('adm.saved'));
} catch (e2) {
setErr(e2 instanceof Error ? e2.message : String(e2));
} finally {
setBusy(false);
}
};
const submit = async (e: React.FormEvent) => {
e.preventDefault();
setBusy(true);
setErr(null);
try {
const r = await api.updateProfile({
email: email.trim(),
currentPassword: current,
...(password ? { password } : {}),
});
keep(r.user);
setPassword('');
setCurrent('');
onSaved(t('adm.saved'));
} catch (e2) {
setErr(e2 instanceof Error ? e2.message : String(e2));
} finally {
setBusy(false);
}
};
return (
<form className="adm-form" data-key="adm-profile" onSubmit={submit}>
<p className="hint">{t('adm.profileHint')}</p>
<div className="avatar-row">
<Avatar user={user} />
<button
type="button"
className="btn"
data-key="adm-avatar-pick"
disabled={busy}
onClick={() => filePick.current?.click()}
>
{t('adm.changeAvatar')}
</button>
<input
ref={filePick}
type="file"
accept="image/png,image/jpeg,image/webp"
hidden
data-key="adm-avatar-file"
onChange={(e) => {
const file = e.target.files?.[0];
e.target.value = '';
if (file) void pick(file);
}}
/>
</div>
<label className="field-label" htmlFor="adm-prof-email">{t('adm.newEmail')}</label>
<input
id="adm-prof-email"
className="field"
type="email"
required
data-key="adm-profile-email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<label className="field-label" htmlFor="adm-prof-current">{t('adm.currentPassword')}</label>
<input
id="adm-prof-current"
className="field"
type="password"
required
data-key="adm-profile-current"
value={current}
onChange={(e) => setCurrent(e.target.value)}
/>
<label className="field-label" htmlFor="adm-prof-password">{t('adm.newPassword')}</label>
<input
id="adm-prof-password"
className="field"
type="password"
minLength={8}
placeholder="••••••••"
data-key="adm-profile-password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
{err ? <p className="err">{err}</p> : null}
<button type="submit" className="btn primary" data-key="adm-profile-save" disabled={busy}>
{busy ? t('auth.busy') : t('adm.save')}
</button>
</form>
);
}