Правки админки, вход

This commit is contained in:
Халимов Рустам
2026-03-31 23:55:38 +03:00
parent bce8f92101
commit 2c6d6f831f
13 changed files with 264 additions and 51 deletions

View File

@@ -543,6 +543,9 @@ export default function AdminPage() {
const [newUser, setNewUser] = useState({ username: '', displayName: '', password: '' });
const [generatedPass, setGeneratedPass] = useState('');
// Delete User Modal State
const [deleteUserModal, setDeleteUserModal] = useState<{ userId: string; username: string } | null>(null);
const [timezones, setTimezones] = useState<TimezoneDto[]>([]);
const [tzSearch, setTzSearch] = useState('');
const [showTzDropdown, setShowTzDropdown] = useState(false);
@@ -616,7 +619,7 @@ export default function AdminPage() {
};
const handleDeleteUser = async (userId: string) => {
if (!confirm(t.confirmDelete)) return;
setDeleteUserModal(null);
try {
await httpClient.request(`/admin/users/${userId}`, { method: 'DELETE' });
showToast(t.successSave, 'success');
@@ -843,7 +846,7 @@ export default function AdminPage() {
<div className="w-16 h-16 rounded-3xl bg-primary/10 border border-primary/20 flex items-center justify-center text-primary">
<span className="material-symbols-outlined text-4xl">hub</span>
</div>
<div className="text-center">
<h1 className="text-3xl font-black font-headline tracking-tighter uppercase mb-2">{t.loginTitle}</h1>
<p className="text-[10px] font-black text-on-surface-variant/40 uppercase tracking-[0.3em]">Administrator</p>
@@ -889,7 +892,7 @@ export default function AdminPage() {
<span className="font-black uppercase tracking-widest text-[14px]">{t.loginBtn}</span>
<span className="material-symbols-outlined text-[20px]">arrow_forward</span>
</button>
<div className="flex gap-4 p-1.5 bg-surface-container-highest/20 rounded-2xl w-full">
<button type="button" onClick={() => setLang('ru')} className={`flex-1 py-3 text-[11px] font-black rounded-xl transition-all ${lang === 'ru' ? 'bg-primary text-on-primary' : 'text-on-surface-variant/40 hover:text-on-surface'}`}>RU</button>
<button type="button" onClick={() => setLang('en')} className={`flex-1 py-3 text-[11px] font-black rounded-xl transition-all ${lang === 'en' ? 'bg-primary text-on-primary' : 'text-on-surface-variant/40 hover:text-on-surface'}`}>EN</button>
@@ -1440,14 +1443,13 @@ export default function AdminPage() {
<div className="flex-1 overflow-y-auto p-2 flex flex-col gap-1">
{users.length === 0 && !isSearching && <div className="text-center py-10 text-gray-500">{t.noUsersFound}</div>}
{users.map(u => (
<button
key={u.id}
onClick={() => fetchUserStats(u.id)}
className={`flex items-center gap-3 p-4 rounded-2xl transition-all text-left group border shadow-sm ${
selectedUser?.id === u.id
? 'bg-primary/10 border-primary/30 scale-[1.01] shadow-primary/5'
: 'hover:bg-surface-container-highest/10 border-transparent'
}`}
<button
key={u.id}
onClick={() => fetchUserStats(u.id)}
className={`flex items-center gap-3 p-4 rounded-2xl transition-all text-left group border shadow-sm ${selectedUser?.id === u.id
? 'bg-primary/10 border-primary/30 scale-[1.01] shadow-primary/5'
: 'hover:bg-surface-container-highest/10 border-transparent'
}`}
>
<div className="relative">
<div className="w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center text-primary font-black shrink-0 slide-on-ice border border-primary/10">
@@ -1535,7 +1537,7 @@ export default function AdminPage() {
{t.blockUser}
</button>
)}
<button onClick={() => handleDeleteUser(selectedUser.id)} className="w-full py-4 rounded-2xl bg-error/5 hover:bg-error/10 text-error/60 hover:text-error font-black uppercase text-[10px] tracking-widest transition-all slide-on-ice">
<button onClick={() => setDeleteUserModal({ userId: selectedUser.id, username: selectedUser.username })} className="w-full py-4 rounded-2xl bg-error/5 hover:bg-error/10 text-error/60 hover:text-error font-black uppercase text-[10px] tracking-widest transition-all slide-on-ice">
{t.deleteUser}
</button>
</div>
@@ -1566,7 +1568,7 @@ export default function AdminPage() {
<label className="flex flex-col gap-2">
<span className="text-[10px] font-black uppercase tracking-widest text-on-surface-variant/40 ml-1">{t.displayName}</span>
<div className="knot-input-group">
<input value={newUser.displayName} onChange={e => setNewUser({ ...newUser, displayName: e.target.value })} placeholder={t.displayNamePlaceholder} />
<input value={newUser.displayName} onChange={e => setNewUser({ ...newUser, displayName: e.target.value })} placeholder={t.displayNamePlaceholder} />
</div>
</label>
<label className="flex flex-col gap-2">
@@ -1588,9 +1590,9 @@ export default function AdminPage() {
{/* Global Toast */}
{toast && (
<motion.div
initial={{ opacity: 0, y: 20, x: '-50%' }}
animate={{ opacity: 1, y: 0, x: '-50%' }}
<motion.div
initial={{ opacity: 0, y: 20, x: '-50%' }}
animate={{ opacity: 1, y: 0, x: '-50%' }}
exit={{ opacity: 0, y: 20, x: '-50%' }}
className="fixed bottom-10 left-1/2 z-[9999] px-6 py-4 rounded-3xl bg-surface-container-high border border-outline-variant/20 text-on-surface shadow-[0_24px_64px_rgba(0,0,0,0.8)] flex items-center gap-4 min-w-[280px] backdrop-blur-3xl"
>
@@ -1605,24 +1607,78 @@ export default function AdminPage() {
</div>
</motion.div>
)}
{/* Delete User Confirmation Modal */}
<AnimatePresence>
{deleteUserModal && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
onClick={() => setDeleteUserModal(null)}
>
<motion.div
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.9, opacity: 0 }}
className="bg-surface-container-high border border-outline-variant/20 rounded-3xl p-8 max-w-md w-full shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center gap-4 mb-6">
<div className="w-12 h-12 rounded-2xl bg-error/10 flex items-center justify-center">
<span className="material-symbols-outlined text-error text-2xl">warning</span>
</div>
<div>
<h3 className="text-xl font-black">{lang === 'ru' ? 'Удаление пользователя' : 'Delete User'}</h3>
<p className="text-sm text-on-surface-variant/60">{lang === 'ru' ? 'Это действие необратимо' : 'This action is irreversible'}</p>
</div>
</div>
<div className="bg-surface-container-lowest/50 rounded-2xl p-4 mb-6">
<p className="text-sm text-on-surface-variant/60 mb-2">{lang === 'ru' ? 'Вы собираетесь удалить:' : 'You are about to delete:'}</p>
<p className="text-lg font-black text-on-surface">@{deleteUserModal.username}</p>
</div>
<p className="text-sm text-on-surface-variant/40 mb-8">
{t.confirmDelete}
</p>
<div className="flex gap-3">
<button
onClick={() => setDeleteUserModal(null)}
className="flex-1 py-4 rounded-2xl bg-surface-container-highest hover:bg-surface-container-high transition-all font-black uppercase text-[12px] tracking-widest"
>
{t.cancel}
</button>
<button
onClick={() => handleDeleteUser(deleteUserModal.userId)}
className="flex-1 py-4 rounded-2xl bg-error hover:bg-error/90 text-white font-black uppercase text-[12px] tracking-widest transition-all"
>
{lang === 'ru' ? 'Удалить' : 'Delete'}
</button>
</div>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
function NavButton({ icon, label, active, onClick }: { icon: string, label: string, active: boolean, onClick: () => void }) {
return (
<button
onClick={onClick}
className={`w-full flex items-center gap-4 px-4 py-3 rounded-2xl transition-all duration-400 ease-ice group relative ${
active
? 'bg-primary/10 text-primary'
: 'text-on-surface-variant/40 hover:text-on-surface hover:bg-surface-container-highest/10'
}`}
<button
onClick={onClick}
className={`w-full flex items-center gap-4 px-4 py-3 rounded-2xl transition-all duration-400 ease-ice group relative ${active
? 'bg-primary/10 text-primary'
: 'text-on-surface-variant/40 hover:text-on-surface hover:bg-surface-container-highest/10'
}`}
>
{active && (
<motion.div
layoutId="nav-active"
className="absolute left-0 w-1 h-6 bg-primary rounded-full"
<motion.div
layoutId="nav-active"
className="absolute left-0 w-1 h-6 bg-primary rounded-full"
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
/>
)}
@@ -1636,18 +1692,16 @@ function NavButton({ icon, label, active, onClick }: { icon: string, label: stri
function Toggle({ checked, onChange }: { checked: boolean, onChange: (v: boolean) => void }) {
return (
<button
onClick={() => onChange(!checked)}
className={`w-11 h-6 rounded-full relative transition-all duration-300 ${
checked ? 'bg-primary' : 'bg-surface-container-highest'
}`}
<button
onClick={() => onChange(!checked)}
className={`w-11 h-6 rounded-full relative transition-all duration-300 ${checked ? 'bg-primary' : 'bg-surface-container-highest'
}`}
>
<motion.div
animate={{ x: checked ? 22 : 3 }}
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
className={`absolute top-1 w-4 h-4 rounded-full ${
checked ? 'bg-surface-container-lowest' : 'bg-on-surface-variant/40'
}`}
<motion.div
animate={{ x: checked ? 22 : 3 }}
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
className={`absolute top-1 w-4 h-4 rounded-full ${checked ? 'bg-surface-container-lowest' : 'bg-on-surface-variant/40'
}`}
/>
</button>
);

View File

@@ -3,17 +3,37 @@ import type { User } from '../../../core/domain/types';
export class AuthApi {
static async login(username: string, password: string) {
return httpClient.request<{ token: string; user: User }>('/auth/login', {
const response = await httpClient.request<{ accessToken: string; userId: string; username: string; displayName: string }>('/auth/login', {
method: 'POST',
body: JSON.stringify({ username, password }),
});
return {
token: response.accessToken,
user: {
id: response.userId,
username: response.username,
displayName: response.displayName,
avatar: null
} as User
};
}
static async register(username: string, displayName: string, password: string, bio?: string) {
return httpClient.request<{ token: string; user: User }>('/auth/register', {
const response = await httpClient.request<{ accessToken: string; userId: string; username: string; displayName: string }>('/auth/register', {
method: 'POST',
body: JSON.stringify({ username, displayName, password, bio }),
});
return {
token: response.accessToken,
user: {
id: response.userId,
username: response.username,
displayName: response.displayName,
avatar: null
} as User
};
}
static async getMe() {