diff --git a/src/components/input/FileInput.tsx b/src/components/input/FileInput.tsx
new file mode 100644
index 0000000..05e6c98
--- /dev/null
+++ b/src/components/input/FileInput.tsx
@@ -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 (
+
+
+ Icon File
+
+
setFile(e.target.files?.[0] ?? null)}
+ />
+
+
+
+ Drop files anywhere or click to select file
+
+ {
+ file && (
+
+ Name: {file.name}
+
+ )
+ }
+
+
+ );
+}
diff --git a/src/components/input/IconInput.tsx b/src/components/input/IconInput.tsx
new file mode 100644
index 0000000..9b5938d
--- /dev/null
+++ b/src/components/input/IconInput.tsx
@@ -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 (
+
+ );
+}
diff --git a/src/hooks/GameHooks.ts b/src/hooks/GameHooks.ts
new file mode 100644
index 0000000..e580217
--- /dev/null
+++ b/src/hooks/GameHooks.ts
@@ -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"] });
+ }
+ });
+}
diff --git a/src/interface/Game.ts b/src/interface/Game.ts
new file mode 100644
index 0000000..846f9ce
--- /dev/null
+++ b/src/interface/Game.ts
@@ -0,0 +1,5 @@
+export interface Game{
+ gameId?: string;
+ gameName: string;
+ gameIcon?: string;
+}
diff --git a/src/pages/protected/AdminPage.tsx b/src/pages/protected/AdminPage.tsx
index c9b70cc..200c3d2 100644
--- a/src/pages/protected/AdminPage.tsx
+++ b/src/pages/protected/AdminPage.tsx
@@ -1,5 +1,6 @@
import TabGroup, { Tab } from "@/components/tab/TabGroup";
import AccountsLoader from "@/ui/account/AccountsLoader";
+import GamesLoader from "@/ui/game/GamesLoader";
export default function AdminPage(){
@@ -7,6 +8,10 @@ export default function AdminPage(){
{
tabHeader: "Accounts",
tabContent:
+ },
+ {
+ tabHeader: "Games",
+ tabContent:
}
];
diff --git a/src/ui/account/AccountsLoader.tsx b/src/ui/account/AccountsLoader.tsx
index b2250f0..a61f2f9 100644
--- a/src/ui/account/AccountsLoader.tsx
+++ b/src/ui/account/AccountsLoader.tsx
@@ -1,4 +1,5 @@
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 { useGetAccounts, useGetAccountsCount } from "@/hooks/AccountHooks";
@@ -12,41 +13,69 @@ export default function AccountsLoader(){
const [ displayCreateAccountModal, setDisplayCreateAccountModal ] = 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 accountsQuery = useGetAccounts(page - 1, pageSize);
const accountsCountQuery = useGetAccountsCount();
useEffect(() => {
- if(accountsCountQuery.isSuccess){
+ if(accountsCountQuery.status === "success"){
setTotalPages(Math.ceil(accountsCountQuery.data / pageSize));
}
}, [ accountsCountQuery ]);
- if(accountsQuery.isLoading){
+ if(accountsQuery.status === "pending"){
return
}
- else if(accountsQuery.isError){
+ else if(accountsQuery.status === "error"){
return Error: {accountsQuery.error.message}
}
else{
return (
<>
- {/* Add Account Button */}
- setDisplayCreateAccountModal(true)}
+
- Create Account
-
-
setDisplayCreateAccountModal(false)}
- account={undefined}
- />
- {/* Account Search Bar */}
+
+
+
+ {/* Add Account Button */}
+
+
setDisplayCreateAccountModal(true)}
+ >
+ Create Account
+
+
setDisplayCreateAccountModal(false)}
+ account={undefined}
+ />
+
+ {/* Account Search Box */}
+
+
+ setSearchTerm(e.target.value)}
+ placeholder="Search"
+ />
+
+
+
+ {/* Account List */}
diff --git a/src/ui/account/modals/AccountModal.tsx b/src/ui/account/modals/AccountModal.tsx
index 9d950ad..bb6c9d9 100644
--- a/src/ui/account/modals/AccountModal.tsx
+++ b/src/ui/account/modals/AccountModal.tsx
@@ -40,22 +40,22 @@ export default function AccountModal({
useEffect(() => {
- if(createAccountMutate.isSuccess){
+ if(createAccountMutate.status === "success"){
createAccountMutate.reset();
addSuccessMessage(`Account ${username} created successfully`);
close();
}
- else if(updateAccountMutate.isSuccess){
+ else if(updateAccountMutate.status === "success"){
updateAccountMutate.reset();
addSuccessMessage(`Account ${username} updated successfully`);
close();
}
- else if(createAccountMutate.isError){
+ else if(createAccountMutate.status === "error"){
createAccountMutate.reset();
addErrorMessage(`Error creating account ${username}: ${createAccountMutate.error.message}`);
console.log(createAccountMutate.error);
}
- else if(updateAccountMutate.isError){
+ else if(updateAccountMutate.status === "error"){
updateAccountMutate.reset();
addErrorMessage(`Error updating account ${username}: ${updateAccountMutate.error.message}`);
console.log(updateAccountMutate.error);
diff --git a/src/ui/account/modals/AccountPasswordResetModal.tsx b/src/ui/account/modals/AccountPasswordResetModal.tsx
index d757f5c..e7c5abd 100644
--- a/src/ui/account/modals/AccountPasswordResetModal.tsx
+++ b/src/ui/account/modals/AccountPasswordResetModal.tsx
@@ -30,12 +30,12 @@ export default function AccountPasswordRestModal({
}
useEffect(() => {
- if(passwordResetMutate.isSuccess){
+ if(passwordResetMutate.status === "success"){
passwordResetMutate.reset();
addSuccessMessage(`Successfully reset password for ${account?.username}`);
close();
}
- else if(passwordResetMutate.isError){
+ else if(passwordResetMutate.status === "error"){
passwordResetMutate.reset();
addErrorMessage(`Failed to reset password for ${account?.username}: ${passwordResetMutate.error.message}`);
console.log(passwordResetMutate.error);
diff --git a/src/ui/account/modals/DeleteAccountModal.tsx b/src/ui/account/modals/DeleteAccountModal.tsx
index 8469600..e0bedbf 100644
--- a/src/ui/account/modals/DeleteAccountModal.tsx
+++ b/src/ui/account/modals/DeleteAccountModal.tsx
@@ -25,12 +25,12 @@ export default function DeleteAccountModal({
}
useEffect(() => {
- if(deleteAccountMutate.isSuccess){
+ if(deleteAccountMutate.status === "success"){
deleteAccountMutate.reset();
addSuccessMessage(`Successfully deleted ${account?.username}`);
close();
}
- else if(deleteAccountMutate.isError){
+ else if(deleteAccountMutate.status === "error"){
deleteAccountMutate.reset();
addErrorMessage(`Error deleting ${account?.username}: ${deleteAccountMutate.error.message}`);
console.log(deleteAccountMutate.error);
diff --git a/src/ui/account/modals/ForcePasswordResetModal.tsx b/src/ui/account/modals/ForcePasswordResetModal.tsx
index c1da695..807a1e8 100644
--- a/src/ui/account/modals/ForcePasswordResetModal.tsx
+++ b/src/ui/account/modals/ForcePasswordResetModal.tsx
@@ -25,12 +25,12 @@ export default function ForcePasswordResetModal({
}
useEffect(() => {
- if(forcePasswordResetMutate.isSuccess){
+ if(forcePasswordResetMutate.status === "success"){
forcePasswordResetMutate.reset();
addSuccessMessage(`Successfully forced password reset for ${account?.username}`);
close();
}
- else if(forcePasswordResetMutate.isError){
+ else if(forcePasswordResetMutate.status === "error"){
forcePasswordResetMutate.reset();
addErrorMessage(`Error forcing password reset for ${account?.username}: ${forcePasswordResetMutate.error.message}`);
console.log(forcePasswordResetMutate.error);
diff --git a/src/ui/account/modals/RevokeRefreshTokenModal.tsx b/src/ui/account/modals/RevokeRefreshTokenModal.tsx
index 8e3f86d..3b480af 100644
--- a/src/ui/account/modals/RevokeRefreshTokenModal.tsx
+++ b/src/ui/account/modals/RevokeRefreshTokenModal.tsx
@@ -25,12 +25,12 @@ export default function RevokeRefreshTokenModal({
}
useEffect(() => {
- if(revokeRefreshTokenMutate.isSuccess){
+ if(revokeRefreshTokenMutate.status === "success"){
revokeRefreshTokenMutate.reset();
addSuccessMessage(`Refresh token for ${account?.username} was successfully revoked`);
close();
}
- else if(revokeRefreshTokenMutate.isError){
+ else if(revokeRefreshTokenMutate.status === "error"){
revokeRefreshTokenMutate.reset();
addErrorMessage(`Error revoking refresh token for ${account?.username}: ${revokeRefreshTokenMutate.error.message}`);
console.log(revokeRefreshTokenMutate.error);
diff --git a/src/ui/game/GameAdminButtons.tsx b/src/ui/game/GameAdminButtons.tsx
new file mode 100644
index 0000000..4092043
--- /dev/null
+++ b/src/ui/game/GameAdminButtons.tsx
@@ -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 (
+
+ );
+}
diff --git a/src/ui/game/GamesList.tsx b/src/ui/game/GamesList.tsx
new file mode 100644
index 0000000..f7dca00
--- /dev/null
+++ b/src/ui/game/GamesList.tsx
@@ -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();
+ const [ displayEditGameModal, setDisplayEditGameModal ] = useState(false);
+ const [ displayDeleteGameModal, setDisplayDeleteGameModal ] = useState(false);
+
+
+ const buttonProps: ButtonProps = {
+ variant: "ghost",
+ size: "md",
+ shape: "square"
+ };
+
+
+ const headElements: React.ReactNode[] = [
+
+ Icon
+
,
+
+ Name
+
,
+
+ Actions
+
+ ];
+
+ const bodyElements: React.ReactNode[][] = games.map((game) => [
+
+ {
+ game.gameIcon &&
+
+

+
+ }
+
+
,
+
+ {game.gameName}
+
,
+
+
+
+
+
{
+ setSelectedGame(game);
+ setDisplayEditGameModal(true);
+ }}
+ showDeleteGameModal={() => {
+ setSelectedGame(game);
+ setDisplayDeleteGameModal(true);
+ }}
+ />
+
+ ]);
+
+
+ return (
+ <>
+
+ {setDisplayEditGameModal(false); setSelectedGame(undefined);}}
+ game={selectedGame}
+ />
+ {setDisplayDeleteGameModal(false); setSelectedGame(undefined);}}
+ game={selectedGame}
+ />
+ >
+ );
+}
diff --git a/src/ui/game/GamesListSkeleton.tsx b/src/ui/game/GamesListSkeleton.tsx
new file mode 100644
index 0000000..f07251e
--- /dev/null
+++ b/src/ui/game/GamesListSkeleton.tsx
@@ -0,0 +1,8 @@
+export default function GamesListSkeleton(){
+ //TODO:
+ return (
+
+ Game List Skeleton
+
+ );
+}
diff --git a/src/ui/game/GamesLoader.tsx b/src/ui/game/GamesLoader.tsx
new file mode 100644
index 0000000..c1c346f
--- /dev/null
+++ b/src/ui/game/GamesLoader.tsx
@@ -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
+ }
+ else if(gamesQuery.status === "error"){
+ return Error {gamesQuery.error.message}
+ }
+ else{
+ return (
+ <>
+
+
+
+
+ {/* Add Game Button */}
+
+
setDisplayCreateGameModal(true)}
+ >
+ Create Game
+
+
setDisplayCreateGameModal(false)}
+ game={undefined}
+ />
+
+ {/* Game Search Box */}
+
+
+ setSearchTerm(e.target.value)}
+ placeholder="Search"
+ />
+
+
+
+ {/* Game List */}
+
+ {/* Pagination */}
+
+ >
+ );
+ }
+}
diff --git a/src/ui/game/modals/DeleteGameModal.tsx b/src/ui/game/modals/DeleteGameModal.tsx
new file mode 100644
index 0000000..cb14316
--- /dev/null
+++ b/src/ui/game/modals/DeleteGameModal.tsx
@@ -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 (
+
+
+ Delete
+
+
+ Cancel
+
+ >
+ }
+ />
+ );
+}
diff --git a/src/ui/game/modals/GameModal.tsx b/src/ui/game/modals/GameModal.tsx
new file mode 100644
index 0000000..279b757
--- /dev/null
+++ b/src/ui/game/modals/GameModal.tsx
@@ -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(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 (
+
+
+ setGameName(e.target.value)}
+ />
+
+ {setIconFile(file); setGameIcon(undefined);}}
+ addErrorMessage={addErrorMessage}
+ />
+
+ }
+ modalFooter={
+ <>
+
+ {game ? "Update" : "Create"}
+
+
+ Cancel
+
+ >
+ }
+ />
+ );
+}