source: chapterx-frontend/src/components/admin/UserTable.tsx@ 99c1e45

main
Last change on this file since 99c1e45 was 99c1e45, checked in by kikisrbinoska <srbinoskakristina07@…>, 11 days ago

Fixed writer section and admin management

  • Property mode set to 100644
File size: 7.0 KB
Line 
1import React, { useState } from 'react'
2import { Search, Shield, UserX, UserCheck } from 'lucide-react'
3import axios from 'axios'
4import { useAuthStore } from '../../store/authStore'
5import { useUIStore } from '../../store/uiStore'
6import { User, UserRole } from '../../types'
7import { Avatar } from '../ui/Avatar'
8import { RoleBadge } from '../ui/Badge'
9import { Button } from '../ui/Button'
10import { Modal } from '../ui/Modal'
11
12const API = 'https://localhost:7125/api'
13
14export const UserTable: React.FC = () => {
15 const { allUsers, updateUserRole, currentUser, token } = useAuthStore()
16 const { addToast } = useUIStore()
17 const [search, setSearch] = useState('')
18 const [confirmUser, setConfirmUser] = useState<User | null>(null)
19 const [confirmAction, setConfirmAction] = useState<'promote' | 'demote' | null>(null)
20 const [loading, setLoading] = useState(false)
21
22 const authHeaders = token ? { Authorization: `Bearer ${token}` } : {}
23
24 const filtered = allUsers.filter(
25 u =>
26 u.username.toLowerCase().includes(search.toLowerCase()) ||
27 u.email.toLowerCase().includes(search.toLowerCase()) ||
28 u.name.toLowerCase().includes(search.toLowerCase())
29 )
30
31 const handlePromote = async (user: User) => {
32 setLoading(true)
33 try {
34 await axios.post(`${API}/admins`, { userId: user.user_id }, { headers: authHeaders })
35 updateUserRole(user.user_id, 'admin')
36 addToast(`${user.username} promoted to admin`)
37 } catch (err: any) {
38 addToast(err?.response?.data?.message || 'Failed to promote user.', 'error')
39 } finally {
40 setLoading(false)
41 setConfirmUser(null)
42 }
43 }
44
45 const handleDemote = async (user: User) => {
46 setLoading(true)
47 try {
48 await axios.delete(`${API}/admins/${user.user_id}`, { headers: authHeaders })
49 updateUserRole(user.user_id, 'writer')
50 addToast(`${user.username} removed from admin`, 'info')
51 } catch (err: any) {
52 addToast(err?.response?.data?.message || 'Failed to demote user.', 'error')
53 } finally {
54 setLoading(false)
55 setConfirmUser(null)
56 }
57 }
58
59 const formatDate = (str: string) =>
60 new Date(str).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
61
62 return (
63 <div>
64 {/* Search */}
65 <div className="relative mb-4">
66 <Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-500" />
67 <input
68 value={search}
69 onChange={e => setSearch(e.target.value)}
70 placeholder="Search users..."
71 className="w-full pl-9 pr-4 py-2.5 bg-slate-800 border border-slate-700 rounded-xl text-sm text-white placeholder-slate-500 focus:outline-none focus:border-indigo-500"
72 />
73 </div>
74
75 {/* Table */}
76 <div className="overflow-x-auto rounded-xl border border-slate-700">
77 <table className="w-full text-sm">
78 <thead>
79 <tr className="border-b border-slate-700 bg-slate-800">
80 <th className="text-left px-4 py-3 text-slate-400 font-medium">User</th>
81 <th className="text-left px-4 py-3 text-slate-400 font-medium hidden md:table-cell">Email</th>
82 <th className="text-left px-4 py-3 text-slate-400 font-medium">Role</th>
83 <th className="text-left px-4 py-3 text-slate-400 font-medium hidden sm:table-cell">Joined</th>
84 <th className="text-right px-4 py-3 text-slate-400 font-medium">Actions</th>
85 </tr>
86 </thead>
87 <tbody className="divide-y divide-slate-800">
88 {filtered.map(user => (
89 <tr key={user.user_id} className="hover:bg-slate-800/50 transition-colors">
90 <td className="px-4 py-3">
91 <div className="flex items-center gap-3">
92 <Avatar name={user.name} size="sm" />
93 <div>
94 <p className="text-white font-medium">{user.name} {user.surname}</p>
95 <p className="text-slate-500 text-xs">@{user.username}</p>
96 </div>
97 </div>
98 </td>
99 <td className="px-4 py-3 text-slate-400 hidden md:table-cell">{user.email}</td>
100 <td className="px-4 py-3"><RoleBadge role={user.role} /></td>
101 <td className="px-4 py-3 text-slate-500 hidden sm:table-cell">{formatDate(user.created_at)}</td>
102 <td className="px-4 py-3">
103 <div className="flex items-center justify-end gap-2">
104 {user.user_id !== currentUser?.user_id && user.role !== 'admin' && (
105 <button
106 onClick={() => { setConfirmUser(user); setConfirmAction('promote') }}
107 className="flex items-center gap-1 text-xs text-indigo-400 hover:text-indigo-300 px-2 py-1 rounded bg-indigo-500/10 hover:bg-indigo-500/20 transition-colors"
108 >
109 <UserCheck size={12} />
110 Promote
111 </button>
112 )}
113 {user.user_id !== currentUser?.user_id && user.role !== 'regular' && (
114 <button
115 onClick={() => { setConfirmUser(user); setConfirmAction('demote') }}
116 className="flex items-center gap-1 text-xs text-rose-400 hover:text-rose-300 px-2 py-1 rounded bg-rose-500/10 hover:bg-rose-500/20 transition-colors"
117 >
118 <UserX size={12} />
119 Demote
120 </button>
121 )}
122 </div>
123 </td>
124 </tr>
125 ))}
126 </tbody>
127 </table>
128 {filtered.length === 0 && (
129 <div className="text-center py-10 text-slate-500">No users found.</div>
130 )}
131 </div>
132
133 {/* Confirm modal */}
134 <Modal
135 isOpen={!!confirmUser && !!confirmAction}
136 onClose={() => { setConfirmUser(null); setConfirmAction(null) }}
137 title={confirmAction === 'promote' ? 'Promote User' : 'Demote User'}
138 size="sm"
139 >
140 {confirmUser && (
141 <div className="space-y-4">
142 <p className="text-slate-300 text-sm">
143 {confirmAction === 'promote'
144 ? `Promote @${confirmUser.username} to the next role tier?`
145 : `Demote @${confirmUser.username} to a lower role?`}
146 </p>
147 <div className="flex gap-3">
148 <Button variant="secondary" className="flex-1" onClick={() => { setConfirmUser(null); setConfirmAction(null) }}>
149 Cancel
150 </Button>
151 <Button
152 variant={confirmAction === 'promote' ? 'primary' : 'danger'}
153 className="flex-1"
154 loading={loading}
155 onClick={() => confirmAction === 'promote' ? handlePromote(confirmUser) : handleDemote(confirmUser)}
156 >
157 <Shield size={14} />
158 Confirm
159 </Button>
160 </div>
161 </div>
162 )}
163 </Modal>
164 </div>
165 )
166}
Note: See TracBrowser for help on using the repository browser.