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.",
|
||||
"roomDeleted": "Raum gelöscht",
|
||||
"roomDeleteFailed": "Raum konnte nicht gelöscht werden",
|
||||
"noRoomsFound": "Keine Räume vorhanden"
|
||||
"noRoomsFound": "Keine Räume vorhanden",
|
||||
"showAllRooms": "Alle {count} Räume anzeigen"
|
||||
},
|
||||
"notifications": {
|
||||
"bell": "Benachrichtigungen",
|
||||
|
||||
@@ -497,7 +497,8 @@
|
||||
"deleteRoomConfirm": "Really delete room \"{name}\"? This cannot be undone.",
|
||||
"roomDeleted": "Room deleted",
|
||||
"roomDeleteFailed": "Room could not be deleted",
|
||||
"noRoomsFound": "No rooms found"
|
||||
"noRoomsFound": "No rooms found",
|
||||
"showAllRooms": "Show all {count} rooms"
|
||||
},
|
||||
"notifications": {
|
||||
"bell": "Notifications",
|
||||
|
||||
@@ -59,6 +59,9 @@ export default function Admin() {
|
||||
const [adminRooms, setAdminRooms] = useState([]);
|
||||
const [adminRoomsLoading, setAdminRoomsLoading] = useState(true);
|
||||
const [roomSearch, setRoomSearch] = useState('');
|
||||
const [roomsExpanded, setRoomsExpanded] = useState(false);
|
||||
const [showAllRoomsModal, setShowAllRoomsModal] = useState(false);
|
||||
const [allRoomsSearch, setAllRoomsSearch] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.role !== 'admin') {
|
||||
@@ -821,10 +824,21 @@ export default function Admin() {
|
||||
|
||||
{/* Room Management */}
|
||||
<div className="card p-6 mb-8">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRoomsExpanded(v => !v)}
|
||||
className="flex items-center justify-between w-full text-left"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<DoorOpen size={20} className="text-th-accent" />
|
||||
<h2 className="text-lg font-semibold text-th-text">{t('admin.roomsTitle')}</h2>
|
||||
<span className="text-sm text-th-text-s">({adminRooms.length})</span>
|
||||
</div>
|
||||
<ChevronDown size={18} className={`text-th-text-s transition-transform duration-200 ${roomsExpanded ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{roomsExpanded && (
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-th-text-s mb-5">{t('admin.roomsDescription')}</p>
|
||||
|
||||
{adminRoomsLoading ? (
|
||||
@@ -833,18 +847,7 @@ export default function Admin() {
|
||||
</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>
|
||||
|
||||
{adminRooms.length > 0 && (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
@@ -867,13 +870,7 @@ export default function Admin() {
|
||||
</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 => (
|
||||
{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>
|
||||
@@ -916,6 +913,18 @@ export default function Admin() {
|
||||
</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">
|
||||
@@ -926,6 +935,8 @@ export default function Admin() {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="card p-4 mb-6">
|
||||
@@ -1204,6 +1215,119 @@ export default function Admin() {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user