import { Button, Chip, Input, Pagination, Select, SelectItem, SortDescriptor, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow, Tooltip, useDisclosure } from "@heroui/react"; import RequestGameModal from "Frontend/components/general/modals/RequestGameModal"; import {ArrowUp, Check, Info, PlusCircle, Trash, X} from "@phosphor-icons/react"; import React, {useEffect, useMemo, useState} from "react"; import {useAuth} from "Frontend/util/auth"; import {ConfigEndpoint, GameRequestEndpoint} from "Frontend/generated/endpoints"; import {gameRequestState} from "Frontend/state/GameRequestState"; import {useSnapshot} from "valtio/react"; import GameRequestDto from "Frontend/generated/org/gameyfin/app/requests/dto/GameRequestDto"; import GameRequestStatus from "Frontend/generated/org/gameyfin/app/requests/status/GameRequestStatus"; import {isAdmin} from "Frontend/util/utils"; import {SmallInfoField} from "Frontend/components/general/SmallInfoField"; export default function GameRequestView() { const rowsPerPage = 25; const auth = useAuth(); const requestGameModal = useDisclosure(); const gameRequests = useSnapshot(gameRequestState).gameRequests; const [areGameRequestsEnabled, setAreGameRequestsEnabled] = useState(false); const [areGuestsAllowedToRequestGames, setAreGuestsAllowedToRequestGames] = useState(false); useEffect(() => { ConfigEndpoint.areGameRequestsEnabled().then(setAreGameRequestsEnabled); ConfigEndpoint.areGuestsAllowedToRequestGames().then(setAreGuestsAllowedToRequestGames); }, []); const [searchTerm, setSearchTerm] = useState(""); const [filters, setFilters] = useState<"all" | GameRequestStatus[]>([GameRequestStatus.PENDING, GameRequestStatus.APPROVED, GameRequestStatus.REJECTED]); const [sortDescriptor, setSortDescriptor] = useState({column: "votes", direction: "descending"}); const [page, setPage] = useState(1); const pages = useMemo(() => { return Math.ceil(getFilteredRequests().length / rowsPerPage); }, [gameRequests, filters]); const filteredItems = useMemo(() => { return getFilteredRequests(); }, [gameRequests, filters, searchTerm]); const sortedItems = useMemo(() => { return (filteredItems as GameRequestDto[]).slice().sort((a, b) => { let cmp: number; switch (sortDescriptor.column) { case "title": cmp = a.title.localeCompare(b.title); break; case "votes": cmp = a.voters.length - b.voters.length; if (cmp === 0) { // If votes are equal, sort by creation date (newest first) cmp = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); } break; case "status": const statusOrder = { [GameRequestStatus.PENDING]: 1, [GameRequestStatus.APPROVED]: 2, [GameRequestStatus.REJECTED]: 3, [GameRequestStatus.FULFILLED]: 4 }; cmp = (statusOrder[a.status] || 99) - (statusOrder[b.status] || 99); break; case "createdAt": cmp = new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); break; case "updatedAt": cmp = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime(); break; default: return 0; // No sorting if the column is not recognized } if (sortDescriptor.direction === "descending") { cmp *= -1; // Reverse the comparison if sorting in descending order } return cmp; }); }, [filteredItems, sortDescriptor]); const pagedItems = useMemo(() => { const start = (page - 1) * rowsPerPage; const end = start + rowsPerPage; return sortedItems.slice(start, end); }, [page, sortedItems]); function getFilteredRequests() { let filteredRequests = (gameRequests as GameRequestDto[]).filter((gameRequest) => { return gameRequest.title.toLowerCase().includes(searchTerm.toLowerCase()) || (gameRequest.requester && gameRequest.requester.username.toLowerCase().includes(searchTerm.toLowerCase())); }); filteredRequests = filteredRequests.filter((gameRequest) => { return filters.includes(gameRequest.status); }); return filteredRequests; } async function toggleVote(gameRequestId: number) { await GameRequestEndpoint.toggleVote(gameRequestId); } async function toggleApprove(gameRequest: GameRequestDto) { if (gameRequest.status == GameRequestStatus.FULFILLED) return; const newStatus = gameRequest.status === GameRequestStatus.APPROVED ? GameRequestStatus.PENDING : GameRequestStatus.APPROVED; await GameRequestEndpoint.changeStatus(gameRequest.id, newStatus); } async function toggleReject(gameRequest: GameRequestDto) { if (gameRequest.status == GameRequestStatus.FULFILLED) return; const newStatus = gameRequest.status === GameRequestStatus.REJECTED ? GameRequestStatus.PENDING : GameRequestStatus.REJECTED; await GameRequestEndpoint.changeStatus(gameRequest.id, newStatus); } async function deleteRequest(gameRequestId: number) { await GameRequestEndpoint.delete(gameRequestId); } function hasUserVotedForRequest(gameRequest: GameRequestDto): boolean { if (!auth.state.user) return false; return gameRequest.voters.map(v => v.id).includes(auth.state.user.id); } function statusToBadge(status: GameRequestStatus) { switch (status) { case GameRequestStatus.APPROVED: return Approved; case GameRequestStatus.FULFILLED: return Fulfilled; case GameRequestStatus.REJECTED: return Rejected; case GameRequestStatus.PENDING: default: return Pending; } } return (<>

Game Requests

{!areGameRequestsEnabled && }
setSearchTerm(e.target.value)} onClear={() => setSearchTerm("")} />
{pagedItems.length > 0 && setPage(page)} />} } > Title & Release Submitted by Submitted Updated Status {/* width={1} keeps the column as far to the right as possible*/} Votes {(item) => ( {item.title} ({item.release ? new Date(item.release).getFullYear() : "unknown"})

{item.requester ? item.requester.username : "Guest" }

{new Date(item.createdAt).toLocaleDateString()} {new Date(item.updatedAt).toLocaleDateString()} {statusToBadge(item.status)}
{isAdmin(auth) &&
} {(isAdmin(auth) || (auth.state.user && item.requester && auth.state.user.id === item.requester.id)) && }
)}
) }