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 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: <AccountsLoader/>
|
||||
},
|
||||
{
|
||||
tabHeader: "Games",
|
||||
tabContent: <GamesLoader/>
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@@ -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,31 +13,44 @@ 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 <AccountsListSkeleton/>
|
||||
}
|
||||
else if(accountsQuery.isError){
|
||||
else if(accountsQuery.status === "error"){
|
||||
return <DangerMessage>Error: {accountsQuery.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 Account Button */}
|
||||
<div
|
||||
className="flex flex-row items-center justify-center w-full"
|
||||
>
|
||||
<PrimaryButton
|
||||
className="mb-8"
|
||||
className="mb-8 tex-tnowrap"
|
||||
onClick={() => setDisplayCreateAccountModal(true)}
|
||||
>
|
||||
Create Account
|
||||
@@ -46,7 +60,22 @@ export default function AccountsLoader(){
|
||||
close={() => setDisplayCreateAccountModal(false)}
|
||||
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
|
||||
accounts={accountsQuery.data ?? []}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
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