sto-k-odnomu/admin/src/pages/RoomsPage.tsx
2026-01-10 00:36:49 +03:00

354 lines
13 KiB
TypeScript

import { useState } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import { roomsApi, type RoomDto } from '@/api/rooms'
import type { AxiosError } from 'axios'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { Badge } from '@/components/ui/badge'
import { Search, Plus, Trash2, ChevronLeft, ChevronRight, ExternalLink } from 'lucide-react'
import { CreateAdminRoomDialog } from '@/components/CreateAdminRoomDialog'
export default function RoomsPage() {
const queryClient = useQueryClient()
const [page, setPage] = useState(1)
const [search, setSearch] = useState('')
const [statusFilter, setStatusFilter] = useState<'WAITING' | 'PLAYING' | 'FINISHED' | ''>('')
const [createDialogOpen, setCreateDialogOpen] = useState(false)
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)
const [roomToDelete, setRoomToDelete] = useState<RoomDto | null>(null)
const limit = 20
// Fetch rooms
const { data, isLoading, error } = useQuery({
queryKey: ['admin', 'rooms', page, statusFilter],
queryFn: () => roomsApi.getRooms({
page,
limit,
status: statusFilter || undefined,
}),
})
// Delete mutation
const deleteMutation = useMutation({
mutationFn: (roomId: string) => roomsApi.deleteRoom(roomId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin', 'rooms'] })
toast.success('Room deleted successfully')
setIsDeleteDialogOpen(false)
setRoomToDelete(null)
},
onError: (error: unknown) => {
const axiosError = error as AxiosError<{ message?: string }>
toast.error(axiosError.response?.data?.message || 'Failed to delete room')
},
})
const handleDelete = (room: RoomDto) => {
setRoomToDelete(room)
setIsDeleteDialogOpen(true)
}
const confirmDelete = () => {
if (roomToDelete?.id) {
deleteMutation.mutate(roomToDelete.id)
}
}
const getStatusBadge = (status: string) => {
switch (status) {
case 'WAITING':
return <Badge variant="secondary">Waiting</Badge>
case 'PLAYING':
return <Badge variant="default">Playing</Badge>
case 'FINISHED':
return <Badge variant="outline">Finished</Badge>
default:
return <Badge>{status}</Badge>
}
}
const filteredRooms = data?.rooms.filter(room =>
search === '' ||
room.code.toLowerCase().includes(search.toLowerCase()) ||
room.host.name?.toLowerCase().includes(search.toLowerCase()) ||
room.host.email?.toLowerCase().includes(search.toLowerCase())
) || []
if (error) {
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">Rooms Management</h1>
<p className="text-muted-foreground">Error loading rooms</p>
</div>
<Card>
<CardContent className="pt-6">
<p className="text-red-500">Failed to load rooms. Please try again later.</p>
</CardContent>
</Card>
</div>
)
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Rooms Management</h1>
<p className="text-muted-foreground">
View and manage game rooms
</p>
</div>
<Button onClick={() => setCreateDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
Create Room
</Button>
</div>
{/* Filters */}
<Card>
<CardContent className="pt-6">
<div className="flex items-center space-x-4">
<div className="flex items-center space-x-2 flex-1">
<Search className="h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search by code, host name or email..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="max-w-sm"
/>
</div>
<div className="flex items-center space-x-2">
<Button
variant={statusFilter === '' ? 'default' : 'outline'}
size="sm"
onClick={() => setStatusFilter('')}
>
All
</Button>
<Button
variant={statusFilter === 'WAITING' ? 'default' : 'outline'}
size="sm"
onClick={() => setStatusFilter('WAITING')}
>
Waiting
</Button>
<Button
variant={statusFilter === 'PLAYING' ? 'default' : 'outline'}
size="sm"
onClick={() => setStatusFilter('PLAYING')}
>
Playing
</Button>
<Button
variant={statusFilter === 'FINISHED' ? 'default' : 'outline'}
size="sm"
onClick={() => setStatusFilter('FINISHED')}
>
Finished
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Rooms Table */}
<Card>
<CardHeader>
<CardTitle>All Rooms ({data?.total || 0})</CardTitle>
<CardDescription>
Manage game rooms and their settings
</CardDescription>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="text-center py-8">Loading rooms...</div>
) : (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead>Code</TableHead>
<TableHead>Status</TableHead>
<TableHead>Host</TableHead>
<TableHead>Theme</TableHead>
<TableHead>Pack</TableHead>
<TableHead>Players</TableHead>
<TableHead>Created</TableHead>
<TableHead>Active Period</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredRooms.length === 0 ? (
<TableRow>
<TableCell colSpan={9} className="text-center py-8 text-muted-foreground">
No rooms found
</TableCell>
</TableRow>
) : (
filteredRooms.map((room) => (
<TableRow key={room.id}>
<TableCell className="font-mono font-medium">
{room.code}
{room.isAdminRoom && (
<Badge variant="secondary" className="ml-2">Admin</Badge>
)}
</TableCell>
<TableCell>{getStatusBadge(room.status)}</TableCell>
<TableCell>
<div>
<div className="font-medium">{room.host.name || 'No name'}</div>
{room.host.email && (
<div className="text-sm text-muted-foreground">{room.host.email}</div>
)}
</div>
</TableCell>
<TableCell>
{room.theme ? (
<Badge variant="outline">{room.theme.name}</Badge>
) : (
<span className="text-muted-foreground">Default</span>
)}
</TableCell>
<TableCell>
{room.questionPack ? (
<span className="text-sm">{room.questionPack.name}</span>
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
<TableCell>
{room._count?.participants || 0} / {room.maxPlayers}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{new Date(room.createdAt).toLocaleDateString()}
</TableCell>
<TableCell className="text-sm text-muted-foreground">
{room.activeFrom && room.activeTo ? (
<div>
<div>{new Date(room.activeFrom).toLocaleDateString()}</div>
<div className="text-xs">to {new Date(room.activeTo).toLocaleDateString()}</div>
</div>
) : (
<span>-</span>
)}
</TableCell>
<TableCell>
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
onClick={() => window.open(`/join/${room.code}`, '_blank')}
>
<ExternalLink className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(room)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
{/* Pagination */}
{data && data.totalPages > 1 && (
<div className="flex items-center justify-between mt-4">
<div className="text-sm text-muted-foreground">
Showing {((page - 1) * limit) + 1} to {Math.min(page * limit, data.total)} of {data.total} rooms
</div>
<div className="flex items-center space-x-2">
<Button
variant="outline"
size="sm"
onClick={() => setPage(page - 1)}
disabled={page <= 1}
>
<ChevronLeft className="h-4 w-4" />
Previous
</Button>
<span className="text-sm">
Page {page} of {data.totalPages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setPage(page + 1)}
disabled={page >= data.totalPages}
>
Next
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
)}
</>
)}
</CardContent>
</Card>
{/* Create Room Dialog */}
{createDialogOpen && (
<CreateAdminRoomDialog
open={createDialogOpen}
onClose={() => setCreateDialogOpen(false)}
onSuccess={() => {
queryClient.invalidateQueries({ queryKey: ['admin', 'rooms'] })
setCreateDialogOpen(false)
}}
/>
)}
{/* Delete Confirmation Dialog */}
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Room</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete the room "{roomToDelete?.code}"? This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={confirmDelete}
className="bg-red-600 hover:bg-red-700"
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)
}