Games tab on admin page working
This commit is contained in:
48
src/components/input/FileInput.tsx
Normal file
48
src/components/input/FileInput.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { BsCloudUpload } from "react-icons/bs";
|
||||||
|
|
||||||
|
export default function FileInput({
|
||||||
|
file,
|
||||||
|
setFile
|
||||||
|
}:{
|
||||||
|
file: File | null | undefined;
|
||||||
|
setFile: (input: File | null) => void;
|
||||||
|
}){
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative border-2 rounded-lg border-gray-500 h-24 mx-4 w-[28rem] z-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="absolute cursor-text left-0 -top-3 bg-white dark:bg-neutral-800 text-gray-500 mx-1 px-1"
|
||||||
|
>
|
||||||
|
Icon File
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
name="iconFile"
|
||||||
|
className="relative opacity-0 w-full h-full z-50 cursor-pointer"
|
||||||
|
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute top-0 left-0 flex flex-col justify-center items-center w-full h-full"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex flex-row gap-2"
|
||||||
|
>
|
||||||
|
<BsCloudUpload
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
Drop files anywhere or click to select file
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
file && (
|
||||||
|
<p
|
||||||
|
className="text-green-600"
|
||||||
|
>
|
||||||
|
Name: {file.name}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
src/components/input/IconInput.tsx
Normal file
37
src/components/input/IconInput.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import FileInput from "./FileInput";
|
||||||
|
|
||||||
|
|
||||||
|
export default function IconInput({
|
||||||
|
file,
|
||||||
|
setFile,
|
||||||
|
addErrorMessage
|
||||||
|
}:{
|
||||||
|
file: File | null | undefined;
|
||||||
|
setFile: (input: File | null) => void;
|
||||||
|
addErrorMessage: (message: string) => void;
|
||||||
|
}){
|
||||||
|
const setIconFile = (inputFile: File | null) => {
|
||||||
|
if((inputFile) && (!inputFile.type.startsWith("image"))){
|
||||||
|
addErrorMessage("File is invalid image format: " + inputFile.type);
|
||||||
|
}
|
||||||
|
//Prevent files larger than 10MB form being uploaded
|
||||||
|
else if((inputFile) && (inputFile.size > 10485760)){
|
||||||
|
addErrorMessage("File is too large: " + inputFile.size + " bytes");
|
||||||
|
}
|
||||||
|
//Prevent empty files
|
||||||
|
else if((inputFile) && (inputFile.size <= 0)){
|
||||||
|
addErrorMessage("File is empty");
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
setFile(inputFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileInput
|
||||||
|
file={file}
|
||||||
|
setFile={setIconFile}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
131
src/hooks/GameHooks.ts
Normal file
131
src/hooks/GameHooks.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { Game } from "@/interface/Game";
|
||||||
|
import { api } from "@/util/AxiosUtil";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
|
||||||
|
export function useGetGames(page: number, pageSize: number, searchTerm?: string){
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["games", { page, pageSize, searchTerm }],
|
||||||
|
queryFn: async () => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("page", page.toString());
|
||||||
|
params.append("pageSize", pageSize.toString());
|
||||||
|
if(searchTerm){
|
||||||
|
params.append("search", searchTerm);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.get(`/game?${params}`);
|
||||||
|
|
||||||
|
if(response.status !== 200){
|
||||||
|
throw new Error("Failed to get games");
|
||||||
|
}
|
||||||
|
else if(response.data.errors){
|
||||||
|
throw new Error(response.data.errors.join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data as Game[];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetGamesCount(){
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["games", "count"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await api.get("/game/count");
|
||||||
|
|
||||||
|
if(response.status !== 200){
|
||||||
|
throw new Error("Failed to get games count");
|
||||||
|
}
|
||||||
|
else if(response.data.errors){
|
||||||
|
throw new Error(response.data.errors.join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data.count as number;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateGame(){
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["createGame"],
|
||||||
|
mutationFn: async ({gameName, iconFile}:{gameName: string, iconFile: File | null}) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
if(iconFile){
|
||||||
|
formData.append("iconFile", iconFile);
|
||||||
|
}
|
||||||
|
formData.append("gameName", gameName);
|
||||||
|
|
||||||
|
const response = await api.post(
|
||||||
|
"/game",
|
||||||
|
formData
|
||||||
|
);
|
||||||
|
|
||||||
|
if(response.status !== 200){
|
||||||
|
throw new Error("Failed to create game");
|
||||||
|
}
|
||||||
|
else if(response.data.errors){
|
||||||
|
throw new Error(response.data.errors.join(", "));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["games"] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateGame(){
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["updateGame"],
|
||||||
|
mutationFn: async ({game, iconFile}:{game: Game, iconFile: File | null}) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
if(iconFile){
|
||||||
|
formData.append("iconFile", iconFile);
|
||||||
|
}
|
||||||
|
formData.append("gameName", game.gameName);
|
||||||
|
if(game.gameIcon){
|
||||||
|
formData.append("gameIcon", game.gameIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await api.put(`/game/${game.gameId}`, formData);
|
||||||
|
|
||||||
|
if(response.status !== 200){
|
||||||
|
throw new Error("Failed to update game");
|
||||||
|
}
|
||||||
|
else if(response.data.errors){
|
||||||
|
throw new Error(response.data.errors.join(", "));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["games"] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteGame(){
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["deleteGame"],
|
||||||
|
mutationFn: async (gameId: string) => {
|
||||||
|
const response = await api.delete(`/game/${gameId}`);
|
||||||
|
|
||||||
|
if(response.status !== 200){
|
||||||
|
throw new Error("Failed to delete game");
|
||||||
|
}
|
||||||
|
else if(response.data.errors){
|
||||||
|
throw new Error(response.data.errors.join(", "));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["games"] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
5
src/interface/Game.ts
Normal file
5
src/interface/Game.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export interface Game{
|
||||||
|
gameId?: string;
|
||||||
|
gameName: string;
|
||||||
|
gameIcon?: string;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import TabGroup, { Tab } from "@/components/tab/TabGroup";
|
import TabGroup, { Tab } from "@/components/tab/TabGroup";
|
||||||
import AccountsLoader from "@/ui/account/AccountsLoader";
|
import AccountsLoader from "@/ui/account/AccountsLoader";
|
||||||
|
import GamesLoader from "@/ui/game/GamesLoader";
|
||||||
|
|
||||||
|
|
||||||
export default function AdminPage(){
|
export default function AdminPage(){
|
||||||
@@ -7,6 +8,10 @@ export default function AdminPage(){
|
|||||||
{
|
{
|
||||||
tabHeader: "Accounts",
|
tabHeader: "Accounts",
|
||||||
tabContent: <AccountsLoader/>
|
tabContent: <AccountsLoader/>
|
||||||
|
},
|
||||||
|
{
|
||||||
|
tabHeader: "Games",
|
||||||
|
tabContent: <GamesLoader/>
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import PrimaryButton from "@/components/button/PrimaryButton";
|
import PrimaryButton from "@/components/button/PrimaryButton";
|
||||||
|
import TextInput from "@/components/input/TextInput";
|
||||||
import DangerMessage from "@/components/message/DangerMessage";
|
import DangerMessage from "@/components/message/DangerMessage";
|
||||||
import Pagination from "@/components/pagination/Pagination";
|
import Pagination from "@/components/pagination/Pagination";
|
||||||
import { useGetAccounts, useGetAccountsCount } from "@/hooks/AccountHooks";
|
import { useGetAccounts, useGetAccountsCount } from "@/hooks/AccountHooks";
|
||||||
@@ -12,31 +13,44 @@ export default function AccountsLoader(){
|
|||||||
const [ displayCreateAccountModal, setDisplayCreateAccountModal ] = useState(false);
|
const [ displayCreateAccountModal, setDisplayCreateAccountModal ] = useState(false);
|
||||||
const [ page, setPage ] = useState(1);
|
const [ page, setPage ] = useState(1);
|
||||||
const [ totalPages, setTotalPages ] = useState(1);
|
const [ totalPages, setTotalPages ] = useState(1);
|
||||||
|
const [ searchTerm, setSearchTerm ] = useState("");
|
||||||
const pageSize = 10;
|
const pageSize = 10;
|
||||||
|
const modalId = crypto.randomUUID().replace("-", "");
|
||||||
|
|
||||||
const accountsQuery = useGetAccounts(page - 1, pageSize);
|
const accountsQuery = useGetAccounts(page - 1, pageSize);
|
||||||
const accountsCountQuery = useGetAccountsCount();
|
const accountsCountQuery = useGetAccountsCount();
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(accountsCountQuery.isSuccess){
|
if(accountsCountQuery.status === "success"){
|
||||||
setTotalPages(Math.ceil(accountsCountQuery.data / pageSize));
|
setTotalPages(Math.ceil(accountsCountQuery.data / pageSize));
|
||||||
}
|
}
|
||||||
}, [ accountsCountQuery ]);
|
}, [ accountsCountQuery ]);
|
||||||
|
|
||||||
|
|
||||||
if(accountsQuery.isLoading){
|
if(accountsQuery.status === "pending"){
|
||||||
return <AccountsListSkeleton/>
|
return <AccountsListSkeleton/>
|
||||||
}
|
}
|
||||||
else if(accountsQuery.isError){
|
else if(accountsQuery.status === "error"){
|
||||||
return <DangerMessage>Error: {accountsQuery.error.message}</DangerMessage>
|
return <DangerMessage>Error: {accountsQuery.error.message}</DangerMessage>
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<div
|
||||||
|
className="flex flex-row justify-between items-center w-full"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex flex-row items-center justify-start w-full"
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
{/* Add Account Button */}
|
{/* Add Account Button */}
|
||||||
|
<div
|
||||||
|
className="flex flex-row items-center justify-center w-full"
|
||||||
|
>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
className="mb-8"
|
className="mb-8 tex-tnowrap"
|
||||||
onClick={() => setDisplayCreateAccountModal(true)}
|
onClick={() => setDisplayCreateAccountModal(true)}
|
||||||
>
|
>
|
||||||
Create Account
|
Create Account
|
||||||
@@ -46,7 +60,22 @@ export default function AccountsLoader(){
|
|||||||
close={() => setDisplayCreateAccountModal(false)}
|
close={() => setDisplayCreateAccountModal(false)}
|
||||||
account={undefined}
|
account={undefined}
|
||||||
/>
|
/>
|
||||||
{/* Account Search Bar */}
|
</div>
|
||||||
|
{/* Account Search Box */}
|
||||||
|
<div
|
||||||
|
className="flex flex-row items-center justify-end w-full"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<TextInput
|
||||||
|
id={`accountSearchBox${modalId}`}
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
placeholder="Search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Account List */}
|
||||||
<AccountsList
|
<AccountsList
|
||||||
accounts={accountsQuery.data ?? []}
|
accounts={accountsQuery.data ?? []}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -40,22 +40,22 @@ export default function AccountModal({
|
|||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(createAccountMutate.isSuccess){
|
if(createAccountMutate.status === "success"){
|
||||||
createAccountMutate.reset();
|
createAccountMutate.reset();
|
||||||
addSuccessMessage(`Account ${username} created successfully`);
|
addSuccessMessage(`Account ${username} created successfully`);
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
else if(updateAccountMutate.isSuccess){
|
else if(updateAccountMutate.status === "success"){
|
||||||
updateAccountMutate.reset();
|
updateAccountMutate.reset();
|
||||||
addSuccessMessage(`Account ${username} updated successfully`);
|
addSuccessMessage(`Account ${username} updated successfully`);
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
else if(createAccountMutate.isError){
|
else if(createAccountMutate.status === "error"){
|
||||||
createAccountMutate.reset();
|
createAccountMutate.reset();
|
||||||
addErrorMessage(`Error creating account ${username}: ${createAccountMutate.error.message}`);
|
addErrorMessage(`Error creating account ${username}: ${createAccountMutate.error.message}`);
|
||||||
console.log(createAccountMutate.error);
|
console.log(createAccountMutate.error);
|
||||||
}
|
}
|
||||||
else if(updateAccountMutate.isError){
|
else if(updateAccountMutate.status === "error"){
|
||||||
updateAccountMutate.reset();
|
updateAccountMutate.reset();
|
||||||
addErrorMessage(`Error updating account ${username}: ${updateAccountMutate.error.message}`);
|
addErrorMessage(`Error updating account ${username}: ${updateAccountMutate.error.message}`);
|
||||||
console.log(updateAccountMutate.error);
|
console.log(updateAccountMutate.error);
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ export default function AccountPasswordRestModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(passwordResetMutate.isSuccess){
|
if(passwordResetMutate.status === "success"){
|
||||||
passwordResetMutate.reset();
|
passwordResetMutate.reset();
|
||||||
addSuccessMessage(`Successfully reset password for ${account?.username}`);
|
addSuccessMessage(`Successfully reset password for ${account?.username}`);
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
else if(passwordResetMutate.isError){
|
else if(passwordResetMutate.status === "error"){
|
||||||
passwordResetMutate.reset();
|
passwordResetMutate.reset();
|
||||||
addErrorMessage(`Failed to reset password for ${account?.username}: ${passwordResetMutate.error.message}`);
|
addErrorMessage(`Failed to reset password for ${account?.username}: ${passwordResetMutate.error.message}`);
|
||||||
console.log(passwordResetMutate.error);
|
console.log(passwordResetMutate.error);
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ export default function DeleteAccountModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(deleteAccountMutate.isSuccess){
|
if(deleteAccountMutate.status === "success"){
|
||||||
deleteAccountMutate.reset();
|
deleteAccountMutate.reset();
|
||||||
addSuccessMessage(`Successfully deleted ${account?.username}`);
|
addSuccessMessage(`Successfully deleted ${account?.username}`);
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
else if(deleteAccountMutate.isError){
|
else if(deleteAccountMutate.status === "error"){
|
||||||
deleteAccountMutate.reset();
|
deleteAccountMutate.reset();
|
||||||
addErrorMessage(`Error deleting ${account?.username}: ${deleteAccountMutate.error.message}`);
|
addErrorMessage(`Error deleting ${account?.username}: ${deleteAccountMutate.error.message}`);
|
||||||
console.log(deleteAccountMutate.error);
|
console.log(deleteAccountMutate.error);
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ export default function ForcePasswordResetModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(forcePasswordResetMutate.isSuccess){
|
if(forcePasswordResetMutate.status === "success"){
|
||||||
forcePasswordResetMutate.reset();
|
forcePasswordResetMutate.reset();
|
||||||
addSuccessMessage(`Successfully forced password reset for ${account?.username}`);
|
addSuccessMessage(`Successfully forced password reset for ${account?.username}`);
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
else if(forcePasswordResetMutate.isError){
|
else if(forcePasswordResetMutate.status === "error"){
|
||||||
forcePasswordResetMutate.reset();
|
forcePasswordResetMutate.reset();
|
||||||
addErrorMessage(`Error forcing password reset for ${account?.username}: ${forcePasswordResetMutate.error.message}`);
|
addErrorMessage(`Error forcing password reset for ${account?.username}: ${forcePasswordResetMutate.error.message}`);
|
||||||
console.log(forcePasswordResetMutate.error);
|
console.log(forcePasswordResetMutate.error);
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ export default function RevokeRefreshTokenModal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(revokeRefreshTokenMutate.isSuccess){
|
if(revokeRefreshTokenMutate.status === "success"){
|
||||||
revokeRefreshTokenMutate.reset();
|
revokeRefreshTokenMutate.reset();
|
||||||
addSuccessMessage(`Refresh token for ${account?.username} was successfully revoked`);
|
addSuccessMessage(`Refresh token for ${account?.username} was successfully revoked`);
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
else if(revokeRefreshTokenMutate.isError){
|
else if(revokeRefreshTokenMutate.status === "error"){
|
||||||
revokeRefreshTokenMutate.reset();
|
revokeRefreshTokenMutate.reset();
|
||||||
addErrorMessage(`Error revoking refresh token for ${account?.username}: ${revokeRefreshTokenMutate.error.message}`);
|
addErrorMessage(`Error revoking refresh token for ${account?.username}: ${revokeRefreshTokenMutate.error.message}`);
|
||||||
console.log(revokeRefreshTokenMutate.error);
|
console.log(revokeRefreshTokenMutate.error);
|
||||||
|
|||||||
38
src/ui/game/GameAdminButtons.tsx
Normal file
38
src/ui/game/GameAdminButtons.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { ButtonProps } from "@/components/button/Button";
|
||||||
|
import DangerButton from "@/components/button/DangerButton";
|
||||||
|
import PrimaryButton from "@/components/button/PrimaryButton";
|
||||||
|
import { BsPencilFill, BsTrash3 } from "react-icons/bs";
|
||||||
|
|
||||||
|
|
||||||
|
export default function GameAdminButtons({
|
||||||
|
buttonProps,
|
||||||
|
showEditGameModal,
|
||||||
|
showDeleteGameModal
|
||||||
|
}:{
|
||||||
|
buttonProps: ButtonProps;
|
||||||
|
showEditGameModal: () => void;
|
||||||
|
showDeleteGameModal: () => void;
|
||||||
|
}){
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex flex-row items-center justify-center gap-2"
|
||||||
|
>
|
||||||
|
<PrimaryButton
|
||||||
|
{...buttonProps}
|
||||||
|
onClick={showEditGameModal}
|
||||||
|
>
|
||||||
|
<BsPencilFill
|
||||||
|
size={22}
|
||||||
|
/>
|
||||||
|
</PrimaryButton>
|
||||||
|
<DangerButton
|
||||||
|
{...buttonProps}
|
||||||
|
onClick={showDeleteGameModal}
|
||||||
|
>
|
||||||
|
<BsTrash3
|
||||||
|
size={22}
|
||||||
|
/>
|
||||||
|
</DangerButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
src/ui/game/GamesList.tsx
Normal file
102
src/ui/game/GamesList.tsx
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { ButtonProps } from "@/components/button/Button";
|
||||||
|
import Table from "@/components/table/Table";
|
||||||
|
import { Game } from "@/interface/Game";
|
||||||
|
import { useState } from "react";
|
||||||
|
import GameAdminButtons from "./GameAdminButtons";
|
||||||
|
import DeleteGameModal from "./modals/DeleteGameModal";
|
||||||
|
import GameModal from "./modals/GameModal";
|
||||||
|
|
||||||
|
|
||||||
|
export default function GamesList({
|
||||||
|
games
|
||||||
|
}:{
|
||||||
|
games: Game[];
|
||||||
|
}){
|
||||||
|
const [ selectedGame, setSelectedGame ] = useState<Game>();
|
||||||
|
const [ displayEditGameModal, setDisplayEditGameModal ] = useState(false);
|
||||||
|
const [ displayDeleteGameModal, setDisplayDeleteGameModal ] = useState(false);
|
||||||
|
|
||||||
|
|
||||||
|
const buttonProps: ButtonProps = {
|
||||||
|
variant: "ghost",
|
||||||
|
size: "md",
|
||||||
|
shape: "square"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const headElements: React.ReactNode[] = [
|
||||||
|
<div>
|
||||||
|
Icon
|
||||||
|
</div>,
|
||||||
|
<div>
|
||||||
|
Name
|
||||||
|
</div>,
|
||||||
|
<div
|
||||||
|
className="pl-16"
|
||||||
|
>
|
||||||
|
Actions
|
||||||
|
</div>
|
||||||
|
];
|
||||||
|
|
||||||
|
const bodyElements: React.ReactNode[][] = games.map((game) => [
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
game.gameIcon &&
|
||||||
|
<div
|
||||||
|
className="absolute -my-4"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="m-auto"
|
||||||
|
src={`${import.meta.env.VITE_ICON_URL}/gameIcons/${game.gameIcon}`}
|
||||||
|
height={56}
|
||||||
|
width={56}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>,
|
||||||
|
<div>
|
||||||
|
{game.gameName}
|
||||||
|
</div>,
|
||||||
|
<div
|
||||||
|
className="flex flex-row items-center justify-center gap-2 pl-16"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="py-4 border-l border-neutral-500"
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<GameAdminButtons
|
||||||
|
buttonProps={buttonProps}
|
||||||
|
showEditGameModal={() => {
|
||||||
|
setSelectedGame(game);
|
||||||
|
setDisplayEditGameModal(true);
|
||||||
|
}}
|
||||||
|
showDeleteGameModal={() => {
|
||||||
|
setSelectedGame(game);
|
||||||
|
setDisplayDeleteGameModal(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Table
|
||||||
|
tableHeadElements={headElements}
|
||||||
|
tableBodyElements={bodyElements}
|
||||||
|
/>
|
||||||
|
<GameModal
|
||||||
|
display={displayEditGameModal}
|
||||||
|
close={() => {setDisplayEditGameModal(false); setSelectedGame(undefined);}}
|
||||||
|
game={selectedGame}
|
||||||
|
/>
|
||||||
|
<DeleteGameModal
|
||||||
|
display={displayDeleteGameModal}
|
||||||
|
close={() => {setDisplayDeleteGameModal(false); setSelectedGame(undefined);}}
|
||||||
|
game={selectedGame}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/ui/game/GamesListSkeleton.tsx
Normal file
8
src/ui/game/GamesListSkeleton.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default function GamesListSkeleton(){
|
||||||
|
//TODO:
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
Game List Skeleton
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
95
src/ui/game/GamesLoader.tsx
Normal file
95
src/ui/game/GamesLoader.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import PrimaryButton from "@/components/button/PrimaryButton";
|
||||||
|
import TextInput from "@/components/input/TextInput";
|
||||||
|
import DangerMessage from "@/components/message/DangerMessage";
|
||||||
|
import Pagination from "@/components/pagination/Pagination";
|
||||||
|
import { useGetGames, useGetGamesCount } from "@/hooks/GameHooks";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import GamesList from "./GamesList";
|
||||||
|
import GamesListSkeleton from "./GamesListSkeleton";
|
||||||
|
import GameModal from "./modals/GameModal";
|
||||||
|
|
||||||
|
|
||||||
|
export default function GamesLoader(){
|
||||||
|
const [ displayCreateGameModal, setDisplayCreateGameModal ] = useState(false);
|
||||||
|
const [ page, setPage ] = useState(1);
|
||||||
|
const [ totalPages, setTotalPages ] = useState(1);
|
||||||
|
const [ searchTerm, setSearchTerm ] = useState("");
|
||||||
|
const pageSize = 10;
|
||||||
|
const modalId = crypto.randomUUID().replace("-", "");
|
||||||
|
|
||||||
|
const gamesQuery = useGetGames(page - 1, pageSize);
|
||||||
|
const gamesCountQuery = useGetGamesCount();
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(gamesCountQuery.status === "success"){
|
||||||
|
setTotalPages(Math.ceil(gamesCountQuery.data / pageSize));
|
||||||
|
}
|
||||||
|
}, [ gamesCountQuery ]);
|
||||||
|
|
||||||
|
|
||||||
|
if(gamesQuery.status === "pending"){
|
||||||
|
return <GamesListSkeleton/>
|
||||||
|
}
|
||||||
|
else if(gamesQuery.status === "error"){
|
||||||
|
return <DangerMessage>Error {gamesQuery.error.message}</DangerMessage>
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="flex flex-row justify-between items-center w-full"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex flex-row items-center justify-start w-full"
|
||||||
|
>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{/* Add Game Button */}
|
||||||
|
<div
|
||||||
|
className="flex flex-row items-center justify-center w-full"
|
||||||
|
>
|
||||||
|
<PrimaryButton
|
||||||
|
className="mb-8"
|
||||||
|
onClick={() => setDisplayCreateGameModal(true)}
|
||||||
|
>
|
||||||
|
Create Game
|
||||||
|
</PrimaryButton>
|
||||||
|
<GameModal
|
||||||
|
display={displayCreateGameModal}
|
||||||
|
close={() => setDisplayCreateGameModal(false)}
|
||||||
|
game={undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Game Search Box */}
|
||||||
|
<div
|
||||||
|
className="flex flex-row items-center justify-end w-full"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<TextInput
|
||||||
|
id={`gameSearchBox${modalId}`}
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
placeholder="Search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Game List */}
|
||||||
|
<GamesList
|
||||||
|
games={gamesQuery.data ?? []}
|
||||||
|
/>
|
||||||
|
{/* Pagination */}
|
||||||
|
<div
|
||||||
|
className="my-12"
|
||||||
|
>
|
||||||
|
<Pagination
|
||||||
|
currentPage={page}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onChange={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/ui/game/modals/DeleteGameModal.tsx
Normal file
62
src/ui/game/modals/DeleteGameModal.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import DangerButton from "@/components/button/DangerButton";
|
||||||
|
import SecondaryButton from "@/components/button/SecondaryButton";
|
||||||
|
import RaidBuilderModal from "@/components/modal/RaidBuilderModal";
|
||||||
|
import { useDeleteGame } from "@/hooks/GameHooks";
|
||||||
|
import { Game } from "@/interface/Game";
|
||||||
|
import { useTimedModal } from "@/providers/TimedModalProvider";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function DeleteGameModal({
|
||||||
|
display,
|
||||||
|
close,
|
||||||
|
game
|
||||||
|
}:{
|
||||||
|
display: boolean;
|
||||||
|
close: () => void;
|
||||||
|
game: Game | undefined;
|
||||||
|
}){
|
||||||
|
const deleteGameMutate = useDeleteGame();
|
||||||
|
const { addSuccessMessage, addErrorMessage } = useTimedModal();
|
||||||
|
|
||||||
|
|
||||||
|
const deleteGame = () => {
|
||||||
|
deleteGameMutate.mutate(game?.gameId ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(deleteGameMutate.status === "success"){
|
||||||
|
deleteGameMutate.reset();
|
||||||
|
addSuccessMessage(`Successfully delete ${game?.gameName}`);
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
else if(deleteGameMutate.status === "error"){
|
||||||
|
deleteGameMutate.reset();
|
||||||
|
addErrorMessage(`Error deleting game ${game?.gameName}: ${deleteGameMutate.error.message}`);
|
||||||
|
console.log(deleteGameMutate.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RaidBuilderModal
|
||||||
|
display={display}
|
||||||
|
close={close}
|
||||||
|
modalHeader={`Delete ${game?.gameName}`}
|
||||||
|
modalBody={`Are you sure you want to delete ${game?.gameName}`}
|
||||||
|
modalFooter={
|
||||||
|
<>
|
||||||
|
<DangerButton
|
||||||
|
onClick={deleteGame}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</DangerButton>
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</SecondaryButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
113
src/ui/game/modals/GameModal.tsx
Normal file
113
src/ui/game/modals/GameModal.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import PrimaryButton from "@/components/button/PrimaryButton";
|
||||||
|
import SecondaryButton from "@/components/button/SecondaryButton";
|
||||||
|
import IconInput from "@/components/input/IconInput";
|
||||||
|
import TextInput from "@/components/input/TextInput";
|
||||||
|
import RaidBuilderModal from "@/components/modal/RaidBuilderModal";
|
||||||
|
import { useCreateGame, useUpdateGame } from "@/hooks/GameHooks";
|
||||||
|
import { Game } from "@/interface/Game";
|
||||||
|
import { useTimedModal } from "@/providers/TimedModalProvider";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
export default function GameModal({
|
||||||
|
display,
|
||||||
|
close,
|
||||||
|
game
|
||||||
|
}:{
|
||||||
|
display: boolean;
|
||||||
|
close: () => void;
|
||||||
|
game?: Game;
|
||||||
|
}){
|
||||||
|
const [ gameName, setGameName ] = useState(game?.gameName);
|
||||||
|
const [ gameIcon, setGameIcon ] = useState(game?.gameIcon);
|
||||||
|
const [ iconFile, setIconFile ] = useState<File | null>(null);
|
||||||
|
const modalId = crypto.randomUUID().replace("-", "");
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setGameName(game?.gameName ?? "");
|
||||||
|
setGameIcon(game?.gameIcon ?? "");
|
||||||
|
}, [ game, setGameName, setGameIcon ]);
|
||||||
|
|
||||||
|
|
||||||
|
const updateGameMutate = useUpdateGame();
|
||||||
|
const createGameMutate = useCreateGame();
|
||||||
|
const { addSuccessMessage, addErrorMessage } = useTimedModal();
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(updateGameMutate.status === "success"){
|
||||||
|
updateGameMutate.reset();
|
||||||
|
addSuccessMessage("Game updated successfully");
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
else if(createGameMutate.status === "success"){
|
||||||
|
createGameMutate.reset();
|
||||||
|
addSuccessMessage("Game created successfully");
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
else if(updateGameMutate.status === "error"){
|
||||||
|
updateGameMutate.reset();
|
||||||
|
addErrorMessage(`Error updating game ${gameName}: ${updateGameMutate.error.message}`);
|
||||||
|
console.log(updateGameMutate.error);
|
||||||
|
}
|
||||||
|
else if(createGameMutate.status === "error"){
|
||||||
|
createGameMutate.reset();
|
||||||
|
addErrorMessage(`Error creating game ${gameName}: ${createGameMutate.error.message}`);
|
||||||
|
console.log(createGameMutate.error);
|
||||||
|
}
|
||||||
|
}, [ updateGameMutate, createGameMutate, gameName, close, addSuccessMessage, addErrorMessage ]);
|
||||||
|
|
||||||
|
|
||||||
|
const updateGame = () => {
|
||||||
|
updateGameMutate.mutate({game: {gameId: game?.gameId, gameName, gameIcon} as Game, iconFile});
|
||||||
|
}
|
||||||
|
|
||||||
|
const createGame = () => {
|
||||||
|
createGameMutate.mutate({gameName: gameName ?? "", iconFile});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RaidBuilderModal
|
||||||
|
display={display}
|
||||||
|
close={close}
|
||||||
|
modalHeader={game ? "Update Game" : "Create Game"}
|
||||||
|
modalBody={
|
||||||
|
<div
|
||||||
|
className="flex flex-col items-center justify-center gap-4"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex flex-row items-center justify-center w-full px-4.5"
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
id={`gameModalGameName${modalId}`}
|
||||||
|
placeholder="Game Name"
|
||||||
|
value={gameName}
|
||||||
|
onChange={(e) => setGameName(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<IconInput
|
||||||
|
file={iconFile}
|
||||||
|
setFile={(file) => {setIconFile(file); setGameIcon(undefined);}}
|
||||||
|
addErrorMessage={addErrorMessage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
modalFooter={
|
||||||
|
<>
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={game ? updateGame : createGame}
|
||||||
|
>
|
||||||
|
{game ? "Update" : "Create"}
|
||||||
|
</PrimaryButton>
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</SecondaryButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user