feat: add functionality to display all rooms with search and modal support in admin panel
All checks were successful
Build & Push Docker Image / build (push) Successful in 4m17s
All checks were successful
Build & Push Docker Image / build (push) Successful in 4m17s
This commit is contained in:
@@ -497,7 +497,8 @@
|
|||||||
"deleteRoomConfirm": "Raum \"{name}\" wirklich löschen? Dies kann nicht rückgängig gemacht werden.",
|
"deleteRoomConfirm": "Raum \"{name}\" wirklich löschen? Dies kann nicht rückgängig gemacht werden.",
|
||||||
"roomDeleted": "Raum gelöscht",
|
"roomDeleted": "Raum gelöscht",
|
||||||
"roomDeleteFailed": "Raum konnte nicht gelöscht werden",
|
"roomDeleteFailed": "Raum konnte nicht gelöscht werden",
|
||||||
"noRoomsFound": "Keine Räume vorhanden"
|
"noRoomsFound": "Keine Räume vorhanden",
|
||||||
|
"showAllRooms": "Alle {count} Räume anzeigen"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"bell": "Benachrichtigungen",
|
"bell": "Benachrichtigungen",
|
||||||
|
|||||||
@@ -497,7 +497,8 @@
|
|||||||
"deleteRoomConfirm": "Really delete room \"{name}\"? This cannot be undone.",
|
"deleteRoomConfirm": "Really delete room \"{name}\"? This cannot be undone.",
|
||||||
"roomDeleted": "Room deleted",
|
"roomDeleted": "Room deleted",
|
||||||
"roomDeleteFailed": "Room could not be deleted",
|
"roomDeleteFailed": "Room could not be deleted",
|
||||||
"noRoomsFound": "No rooms found"
|
"noRoomsFound": "No rooms found",
|
||||||
|
"showAllRooms": "Show all {count} rooms"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"bell": "Notifications",
|
"bell": "Notifications",
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ export default function Admin() {
|
|||||||
const [adminRooms, setAdminRooms] = useState([]);
|
const [adminRooms, setAdminRooms] = useState([]);
|
||||||
const [adminRoomsLoading, setAdminRoomsLoading] = useState(true);
|
const [adminRoomsLoading, setAdminRoomsLoading] = useState(true);
|
||||||
const [roomSearch, setRoomSearch] = useState('');
|
const [roomSearch, setRoomSearch] = useState('');
|
||||||
|
const [roomsExpanded, setRoomsExpanded] = useState(false);
|
||||||
|
const [showAllRoomsModal, setShowAllRoomsModal] = useState(false);
|
||||||
|
const [allRoomsSearch, setAllRoomsSearch] = useState('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user?.role !== 'admin') {
|
if (user?.role !== 'admin') {
|
||||||
@@ -821,109 +824,117 @@ export default function Admin() {
|
|||||||
|
|
||||||
{/* Room Management */}
|
{/* Room Management */}
|
||||||
<div className="card p-6 mb-8">
|
<div className="card p-6 mb-8">
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<button
|
||||||
<DoorOpen size={20} className="text-th-accent" />
|
type="button"
|
||||||
<h2 className="text-lg font-semibold text-th-text">{t('admin.roomsTitle')}</h2>
|
onClick={() => setRoomsExpanded(v => !v)}
|
||||||
</div>
|
className="flex items-center justify-between w-full text-left"
|
||||||
<p className="text-sm text-th-text-s mb-5">{t('admin.roomsDescription')}</p>
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
{adminRoomsLoading ? (
|
<DoorOpen size={20} className="text-th-accent" />
|
||||||
<div className="flex justify-center py-4">
|
<h2 className="text-lg font-semibold text-th-text">{t('admin.roomsTitle')}</h2>
|
||||||
<Loader2 size={20} className="animate-spin text-th-accent" />
|
<span className="text-sm text-th-text-s">({adminRooms.length})</span>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<ChevronDown size={18} className={`text-th-text-s transition-transform duration-200 ${roomsExpanded ? 'rotate-180' : ''}`} />
|
||||||
<>
|
</button>
|
||||||
{/* 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">
|
{roomsExpanded && (
|
||||||
<table className="w-full">
|
<div className="mt-4">
|
||||||
<thead>
|
<p className="text-sm text-th-text-s mb-5">{t('admin.roomsDescription')}</p>
|
||||||
<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 && (
|
{adminRoomsLoading ? (
|
||||||
<div className="text-center py-8">
|
<div className="flex justify-center py-4">
|
||||||
<DoorOpen size={36} className="mx-auto text-th-text-s/40 mb-2" />
|
<Loader2 size={20} className="animate-spin text-th-accent" />
|
||||||
<p className="text-th-text-s text-sm">{t('admin.noRoomsFound')}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{adminRooms.length > 0 && (
|
||||||
|
<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.slice(0, 10).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 > 10 && (
|
||||||
|
<div className="mt-4 text-center">
|
||||||
|
<button
|
||||||
|
onClick={() => { setAllRoomsSearch(''); setShowAllRoomsModal(true); }}
|
||||||
|
className="btn-secondary text-sm"
|
||||||
|
>
|
||||||
|
{t('admin.showAllRooms', { count: adminRooms.length })}
|
||||||
|
</button>
|
||||||
|
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1204,6 +1215,119 @@ export default function Admin() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* All rooms modal */}
|
||||||
|
{showAllRoomsModal && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setShowAllRoomsModal(false)} />
|
||||||
|
<div className="relative bg-th-card rounded-2xl border border-th-border shadow-2xl w-full max-w-4xl max-h-[85vh] flex flex-col">
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-th-border">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<DoorOpen size={20} className="text-th-accent" />
|
||||||
|
<h3 className="text-lg font-semibold text-th-text">{t('admin.roomsTitle')}</h3>
|
||||||
|
<span className="text-sm text-th-text-s">({adminRooms.length})</span>
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setShowAllRoomsModal(false)} className="p-1.5 rounded-lg hover:bg-th-hover text-th-text-s transition-colors">
|
||||||
|
<XIcon size={18} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 border-b border-th-border">
|
||||||
|
<div className="relative">
|
||||||
|
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-th-text-s" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={allRoomsSearch}
|
||||||
|
onChange={e => setAllRoomsSearch(e.target.value)}
|
||||||
|
className="input-field pl-9 text-sm"
|
||||||
|
placeholder={t('admin.searchRooms')}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-y-auto flex-1 p-0">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="sticky top-0 bg-th-card z-10">
|
||||||
|
<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(allRoomsSearch.toLowerCase()) ||
|
||||||
|
r.owner_name.toLowerCase().includes(allRoomsSearch.toLowerCase()) ||
|
||||||
|
r.uid.toLowerCase().includes(allRoomsSearch.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={() => { setShowAllRoomsModal(false); 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>
|
||||||
|
{adminRooms.filter(r =>
|
||||||
|
r.name.toLowerCase().includes(allRoomsSearch.toLowerCase()) ||
|
||||||
|
r.owner_name.toLowerCase().includes(allRoomsSearch.toLowerCase()) ||
|
||||||
|
r.uid.toLowerCase().includes(allRoomsSearch.toLowerCase())
|
||||||
|
).length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user