feat: add UserManagementPage component

This commit is contained in:
2026-05-10 13:09:30 +03:00
parent 3bbcf7e256
commit 4b244bf0bb
+274
View File
@@ -0,0 +1,274 @@
import { useState } from 'react';
import { Users, Trash2 } from 'lucide-react';
import { useAdminUsers } from './useAdminUsers';
const inputStyle = {
padding: '10px 12px',
background: 'var(--bg-lighter)',
border: '1.5px solid var(--border-light)',
borderRadius: '6px',
color: 'var(--text-main)',
fontSize: '0.92rem',
outline: 'none',
fontFamily: 'var(--font-body)',
boxSizing: 'border-box',
flex: 1,
};
const labelStyle = {
display: 'block',
marginBottom: '5px',
fontSize: '0.75rem',
fontWeight: 700,
color: 'var(--text-muted)',
textTransform: 'uppercase',
letterSpacing: '0.4px',
};
const thStyle = {
padding: '10px 14px',
textAlign: 'left',
fontSize: '0.72rem',
fontWeight: 700,
color: 'var(--text-muted)',
textTransform: 'uppercase',
letterSpacing: '0.4px',
borderBottom: '1.5px solid var(--border-light)',
fontFamily: 'var(--font-heading)',
};
const tdStyle = {
padding: '12px 14px',
fontSize: '0.88rem',
color: 'var(--text-main)',
borderBottom: '1px solid var(--border-light)',
fontFamily: 'var(--font-body)',
};
const formatDate = (dateStr) => {
if (!dateStr) return '—';
return new Date(dateStr).toLocaleDateString('ru-RU');
};
const UserManagementPage = () => {
const { users, loading, error, inviteUser, deleteUser } = useAdminUsers();
const [inviteEmail, setInviteEmail] = useState('');
const [inviting, setInviting] = useState(false);
const [inviteError, setInviteError] = useState('');
const [confirmDeleteId, setConfirmDeleteId] = useState(null);
const [deleting, setDeleting] = useState(false);
const handleInvite = async (e) => {
e.preventDefault();
setInviteError('');
setInviting(true);
try {
await inviteUser(inviteEmail);
setInviteEmail('');
} catch (err) {
setInviteError(err.message ?? 'Ошибка приглашения');
} finally {
setInviting(false);
}
};
const handleDeleteConfirm = async (userId) => {
setDeleting(true);
try {
await deleteUser(userId);
} catch {
// deletion errors surface in the hook's error state
} finally {
setDeleting(false);
setConfirmDeleteId(null);
}
};
return (
<div style={{ padding: '30px', maxWidth: '760px' }}>
<h1
style={{
fontFamily: 'var(--font-heading)',
fontSize: '1.5rem',
fontWeight: 800,
marginBottom: '24px',
display: 'flex',
alignItems: 'center',
gap: '10px',
}}
>
<Users size={22} color="var(--accent-blue)" />
Пользователи
</h1>
{/* Invite form */}
<div className="glass-panel" style={{ padding: '24px', marginBottom: '24px' }}>
<h2
style={{
fontFamily: 'var(--font-heading)',
fontSize: '1rem',
fontWeight: 700,
marginBottom: '16px',
}}
>
Пригласить пользователя
</h2>
<form onSubmit={handleInvite}>
<label style={labelStyle} htmlFor="invite-email">
Email
</label>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<input
id="invite-email"
type="email"
placeholder="email@example.com"
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
required
style={inputStyle}
/>
<button
type="submit"
disabled={inviting}
className="btn-primary"
style={{
whiteSpace: 'nowrap',
opacity: inviting ? 0.7 : 1,
cursor: inviting ? 'not-allowed' : 'pointer',
}}
>
{inviting ? 'Отправка...' : 'Пригласить'}
</button>
</div>
{inviteError && (
<p style={{ color: '#ef4444', fontSize: '0.85rem', marginTop: '10px' }}>
{inviteError}
</p>
)}
</form>
</div>
{/* Users table */}
<div className="glass-panel" style={{ padding: '24px' }}>
<h2
style={{
fontFamily: 'var(--font-heading)',
fontSize: '1rem',
fontWeight: 700,
marginBottom: '16px',
}}
>
Администраторы
</h2>
{error && (
<p style={{ color: '#ef4444', fontSize: '0.85rem', marginBottom: '12px' }}>
{error}
</p>
)}
{loading ? (
<div style={{ color: 'var(--text-muted)', padding: '10px 0' }}>Загрузка...</div>
) : (
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr>
<th style={thStyle}>Email</th>
<th style={thStyle}>Дата создания</th>
<th style={thStyle}>Последний вход</th>
<th style={{ ...thStyle, textAlign: 'right' }}></th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id}>
<td style={tdStyle}>{user.email}</td>
<td style={tdStyle}>{formatDate(user.created_at)}</td>
<td style={tdStyle}>{formatDate(user.last_sign_in_at)}</td>
<td style={{ ...tdStyle, textAlign: 'right', whiteSpace: 'nowrap' }}>
{confirmDeleteId === user.id ? (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '0.82rem', color: 'var(--text-muted)' }}>
Вы уверены?
</span>
<button
onClick={() => handleDeleteConfirm(user.id)}
disabled={deleting}
style={{
padding: '4px 10px',
background: '#ef4444',
color: '#fff',
border: 'none',
borderRadius: '5px',
fontSize: '0.78rem',
fontWeight: 700,
cursor: deleting ? 'not-allowed' : 'pointer',
opacity: deleting ? 0.7 : 1,
fontFamily: 'var(--font-heading)',
}}
>
Удалить
</button>
<button
onClick={() => setConfirmDeleteId(null)}
disabled={deleting}
style={{
padding: '4px 10px',
background: 'transparent',
color: 'var(--text-muted)',
border: '1.5px solid var(--border-light)',
borderRadius: '5px',
fontSize: '0.78rem',
fontWeight: 700,
cursor: 'pointer',
fontFamily: 'var(--font-heading)',
}}
>
Отмена
</button>
</span>
) : (
<button
onClick={() => setConfirmDeleteId(user.id)}
title="Удалить пользователя"
style={{
background: 'transparent',
border: 'none',
cursor: 'pointer',
color: 'var(--text-muted)',
padding: '4px',
borderRadius: '4px',
display: 'inline-flex',
alignItems: 'center',
}}
>
<Trash2 size={15} />
</button>
)}
</td>
</tr>
))}
{users.length === 0 && (
<tr>
<td
colSpan={4}
style={{ ...tdStyle, color: 'var(--text-muted)', textAlign: 'center', padding: '20px' }}
>
Нет пользователей
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</div>
</div>
);
};
export default UserManagementPage;