feat: add room management functionality for admins with listing and deletion options
All checks were successful
Build & Push Docker Image / build (push) Successful in 4m12s
All checks were successful
Build & Push Docker Image / build (push) Successful in 4m12s
This commit is contained in:
@@ -4,7 +4,7 @@ import {
|
||||
Users, Shield, Search, Trash2, ChevronDown, Loader2,
|
||||
MoreVertical, Key, UserCheck, UserX, UserPlus, Mail, Lock, User,
|
||||
Upload, X as XIcon, Image, Type, Palette, Send, Copy, Clock, Check,
|
||||
ShieldCheck, Globe, Link as LinkIcon, LogIn,
|
||||
ShieldCheck, Globe, Link as LinkIcon, LogIn, DoorOpen, Eye, ExternalLink,
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
@@ -55,6 +55,11 @@ export default function Admin() {
|
||||
const [oauthForm, setOauthForm] = useState({ issuer: '', clientId: '', clientSecret: '', displayName: 'SSO', autoRegister: true });
|
||||
const [savingOauth, setSavingOauth] = useState(false);
|
||||
|
||||
// Rooms state
|
||||
const [adminRooms, setAdminRooms] = useState([]);
|
||||
const [adminRoomsLoading, setAdminRoomsLoading] = useState(true);
|
||||
const [roomSearch, setRoomSearch] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.role !== 'admin') {
|
||||
navigate('/dashboard');
|
||||
@@ -63,6 +68,7 @@ export default function Admin() {
|
||||
fetchUsers();
|
||||
fetchInvites();
|
||||
fetchOauthConfig();
|
||||
fetchAdminRooms();
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -101,6 +107,29 @@ export default function Admin() {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAdminRooms = async () => {
|
||||
setAdminRoomsLoading(true);
|
||||
try {
|
||||
const res = await api.get('/admin/rooms');
|
||||
setAdminRooms(res.data.rooms);
|
||||
} catch {
|
||||
// silently fail
|
||||
} finally {
|
||||
setAdminRoomsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAdminDeleteRoom = async (uid, name) => {
|
||||
if (!confirm(t('admin.deleteRoomConfirm', { name }))) return;
|
||||
try {
|
||||
await api.delete(`/rooms/${uid}`);
|
||||
toast.success(t('admin.roomDeleted'));
|
||||
fetchAdminRooms();
|
||||
} catch (err) {
|
||||
toast.error(err.response?.data?.error || t('admin.roomDeleteFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleRoleChange = async (userId, newRole) => {
|
||||
try {
|
||||
await api.put(`/admin/users/${userId}/role`, { role: newRole });
|
||||
@@ -790,6 +819,114 @@ export default function Admin() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Room Management */}
|
||||
<div className="card p-6 mb-8">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<DoorOpen size={20} className="text-th-accent" />
|
||||
<h2 className="text-lg font-semibold text-th-text">{t('admin.roomsTitle')}</h2>
|
||||
</div>
|
||||
<p className="text-sm text-th-text-s mb-5">{t('admin.roomsDescription')}</p>
|
||||
|
||||
{adminRoomsLoading ? (
|
||||
<div className="flex justify-center py-4">
|
||||
<Loader2 size={20} className="animate-spin text-th-accent" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Room search */}
|
||||
<div className="relative mb-4">
|
||||
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-th-text-s" />
|
||||
<input
|
||||
type="text"
|
||||
value={roomSearch}
|
||||
onChange={e => setRoomSearch(e.target.value)}
|
||||
className="input-field pl-9 text-sm"
|
||||
placeholder={t('admin.searchRooms')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-th-border">
|
||||
<th className="text-left text-xs font-semibold text-th-text-s uppercase tracking-wider px-4 py-2.5">
|
||||
{t('admin.roomName')}
|
||||
</th>
|
||||
<th className="text-left text-xs font-semibold text-th-text-s uppercase tracking-wider px-4 py-2.5 hidden sm:table-cell">
|
||||
{t('admin.roomOwner')}
|
||||
</th>
|
||||
<th className="text-left text-xs font-semibold text-th-text-s uppercase tracking-wider px-4 py-2.5 hidden md:table-cell">
|
||||
{t('admin.roomShares')}
|
||||
</th>
|
||||
<th className="text-left text-xs font-semibold text-th-text-s uppercase tracking-wider px-4 py-2.5 hidden lg:table-cell">
|
||||
{t('admin.roomCreated')}
|
||||
</th>
|
||||
<th className="text-right text-xs font-semibold text-th-text-s uppercase tracking-wider px-4 py-2.5">
|
||||
{t('admin.actions')}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{adminRooms
|
||||
.filter(r =>
|
||||
r.name.toLowerCase().includes(roomSearch.toLowerCase()) ||
|
||||
r.owner_name.toLowerCase().includes(roomSearch.toLowerCase()) ||
|
||||
r.uid.toLowerCase().includes(roomSearch.toLowerCase())
|
||||
)
|
||||
.map(r => (
|
||||
<tr key={r.id} className="border-b border-th-border last:border-0 hover:bg-th-hover transition-colors">
|
||||
<td className="px-4 py-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-th-text">{r.name}</p>
|
||||
<p className="text-xs text-th-text-s font-mono">{r.uid}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 hidden sm:table-cell">
|
||||
<div>
|
||||
<p className="text-sm text-th-text">{r.owner_name}</p>
|
||||
<p className="text-xs text-th-text-s">{r.owner_email}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-th-text hidden md:table-cell">
|
||||
{r.share_count}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-th-text-s hidden lg:table-cell">
|
||||
{new Date(r.created_at).toLocaleDateString(language === 'de' ? 'de-DE' : 'en-US')}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<button
|
||||
onClick={() => navigate(`/rooms/${r.uid}`)}
|
||||
className="p-1.5 rounded-lg hover:bg-th-hover text-th-text-s transition-colors"
|
||||
title={t('admin.roomView')}
|
||||
>
|
||||
<Eye size={15} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleAdminDeleteRoom(r.uid, r.name)}
|
||||
className="p-1.5 rounded-lg hover:bg-th-hover text-th-error transition-colors"
|
||||
title={t('admin.deleteRoom')}
|
||||
>
|
||||
<Trash2 size={15} />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{adminRooms.length === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<DoorOpen size={36} className="mx-auto text-th-text-s/40 mb-2" />
|
||||
<p className="text-th-text-s text-sm">{t('admin.noRoomsFound')}</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="card p-4 mb-6">
|
||||
<div className="relative">
|
||||
|
||||
Reference in New Issue
Block a user