From b78b6109b3817166dcb51488334cdb9f3fb7070d Mon Sep 17 00:00:00 2001 From: Mattrixwv Date: Wed, 5 Mar 2025 20:12:10 -0500 Subject: [PATCH] Admin page raid groups tab working --- src/components/game/GameSelector.tsx | 100 +++++++++++++ src/hooks/GameHooks.ts | 21 ++- src/hooks/RaidGroupHooks.ts | 139 ++++++++++++++++++ src/interface/RaidGroup.ts | 6 + src/pages/protected/AdminPage.tsx | 5 + src/ui/game/modals/DeleteGameModal.tsx | 1 + src/ui/raidGroup/AdminRaidGroupTab.tsx | 96 ++++++++++++ src/ui/raidGroup/RaidGroupAdminButtons.tsx | 38 +++++ src/ui/raidGroup/RaidGroupsList.tsx | 102 +++++++++++++ src/ui/raidGroup/RaidGroupsListSkeleton.tsx | 7 + src/ui/raidGroup/RaidGroupsLoader.tsx | 31 ++++ .../raidGroup/modals/DeleteRaidGroupModal.tsx | 63 ++++++++ src/ui/raidGroup/modals/RaidGroupModal.tsx | 130 ++++++++++++++++ 13 files changed, 738 insertions(+), 1 deletion(-) create mode 100644 src/components/game/GameSelector.tsx create mode 100644 src/hooks/RaidGroupHooks.ts create mode 100644 src/interface/RaidGroup.ts create mode 100644 src/ui/raidGroup/AdminRaidGroupTab.tsx create mode 100644 src/ui/raidGroup/RaidGroupAdminButtons.tsx create mode 100644 src/ui/raidGroup/RaidGroupsList.tsx create mode 100644 src/ui/raidGroup/RaidGroupsListSkeleton.tsx create mode 100644 src/ui/raidGroup/RaidGroupsLoader.tsx create mode 100644 src/ui/raidGroup/modals/DeleteRaidGroupModal.tsx create mode 100644 src/ui/raidGroup/modals/RaidGroupModal.tsx diff --git a/src/components/game/GameSelector.tsx b/src/components/game/GameSelector.tsx new file mode 100644 index 0000000..151c073 --- /dev/null +++ b/src/components/game/GameSelector.tsx @@ -0,0 +1,100 @@ +import { useGetGames } from "@/hooks/GameHooks"; +import { Game } from "@/interface/Game"; +import clsx from "clsx"; +import { useEffect, useState } from "react"; +import { useDebouncedCallback } from "use-debounce"; +import TextInput from "../input/TextInput"; + +export default function GameSelector({ + disabled, + game, + onChange +}:{ + disabled: boolean; + game?: Game; + onChange: (game: Game | undefined) => void; +}){ + const [ gameSearch, setGameSearch ] = useState(game?.gameName ?? ""); + const [ searchTerm, setSearchTerm ] = useState(game?.gameName ?? ""); + const [ searching, setSearching ] = useState(false); + + const modalId = crypto.randomUUID().replace("-", ""); + + + const gameSearchQuery = useGetGames(0, 5, gameSearch); + const games = gameSearchQuery.data; + + const setGame = (selectedGame: Game) => { + setSearchTerm(selectedGame.gameName ?? ""); + setGameSearch(selectedGame.gameName ?? ""); + setSearching(false); + onChange?.(selectedGame); + } + + + const updateGameSearch = useDebouncedCallback((searchTerm: string) => { + setGameSearch(searchTerm); + }, 500); + + + useEffect(() => { + updateGameSearch(searchTerm); + }, [ searchTerm, updateGameSearch ]); + + + return ( +
+ { setSearchTerm(e.target.value); setSearching(true); }} + value={searchTerm} + disabled={disabled} + /> +
+
+ { + games && games.map((searchGame, index) => ( +
0 && index < games.length - 1, + } + )} + onClick={() => setGame(searchGame)} + > +
+ {searchGame.gameName} +
+
+ )) + } + { + games?.length == 0 && +
+ No games found +
+ } +
+
+
+ ); +} diff --git a/src/hooks/GameHooks.ts b/src/hooks/GameHooks.ts index 1b85db3..8a4d6cd 100644 --- a/src/hooks/GameHooks.ts +++ b/src/hooks/GameHooks.ts @@ -3,6 +3,25 @@ import { api } from "@/util/AxiosUtil"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +export function useGetGame(gameId: string, disabled: boolean){ + return useQuery({ + queryKey: ["game", gameId], + queryFn: async () => { + const response = await api.get(`/game/${gameId}`); + + if(response.status !== 200){ + throw new Error("Failed to get game"); + } + else if(response.data.errors){ + throw new Error(response.data.errors.join(", ")); + } + + return response.data as Game; + }, + enabled: !disabled + }); +} + export function useGetGames(page: number, pageSize: number, searchTerm?: string){ return useQuery({ queryKey: ["games", { page, pageSize, searchTerm }], @@ -58,7 +77,7 @@ export function useCreateGame(){ return useMutation({ mutationKey: ["createGame"], - mutationFn: async ({gameName, iconFile}:{gameName: string, iconFile: File | null}) => { + mutationFn: async ({gameName, iconFile}:{gameName: string; iconFile: File | null;}) => { const formData = new FormData(); if(iconFile){ formData.append("iconFile", iconFile); diff --git a/src/hooks/RaidGroupHooks.ts b/src/hooks/RaidGroupHooks.ts new file mode 100644 index 0000000..699387c --- /dev/null +++ b/src/hooks/RaidGroupHooks.ts @@ -0,0 +1,139 @@ +import { RaidGroup } from "@/interface/RaidGroup"; +import { api } from "@/util/AxiosUtil"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + + +export function useGetRaidGroups(page: number, pageSize: number, searchTerm?: string){ + return useQuery({ + queryKey: ["raidGroups", { page, pageSize, searchTerm }], + queryFn: async () => { + const params = new URLSearchParams(); + params.append("page", page.toString()); + params.append("pageSize", pageSize.toString()); + if(searchTerm){ + params.append("searchTerm", searchTerm); + } + + const response = await api.get(`/raidGroup?${params}`); + + if(response.status !== 200){ + throw new Error("Failed to get raid groups"); + } + else if(response.data.errors){ + throw new Error(response.data.errors.join(", ")); + } + + return response.data as RaidGroup[]; + } + }); +} + +export function useGetRaidGroupsCount(searchTerm?: string){ + const searchParams = new URLSearchParams(); + if(searchTerm){ + searchParams.append("searchTerm", searchTerm); + } + + + return useQuery({ + queryKey: ["raidGroups", "count", searchTerm], + queryFn: async () => { + const response = await api.get(`/raidGroup/count?${searchParams}`); + + if(response.status !== 200){ + throw new Error("Failed to get raid groups count"); + } + else if(response.data.errors){ + throw new Error(response.data.errors.join(", ")); + } + + return response.data.count as number; + } + }); +} + +export function useCreateRaidGroup(){ + const queryClient = useQueryClient(); + + + return useMutation({ + mutationKey: ["createRaidGroup"], + mutationFn: async ({raidGroupName, gameId, iconFile}:{raidGroupName: string; gameId: string; iconFile: File | null;}) => { + const formData = new FormData(); + if(iconFile){ + formData.append("iconFile", iconFile); + } + formData.append("raidGroupName", raidGroupName); + formData.append("gameId", gameId); + + const response = await api.post( + "/raidGroup", + formData + ); + + if(response.status !== 200){ + throw new Error("Failed to create raid group"); + } + else if(response.data.errors){ + throw new Error(response.data.errors.join(", ")); + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["raidGroups"] }); + } + }); +} + +export function useUpdateRaidGroup(){ + const queryClient = useQueryClient(); + + + return useMutation({ + mutationKey: ["updateRaidGroup"], + mutationFn: async ({raidGroup, iconFile}:{raidGroup: RaidGroup; iconFile: File | null;}) => { + const formData = new FormData(); + if(iconFile){ + formData.append("iconFile", iconFile); + } + formData.append("raidGroupName", raidGroup.raidGroupName); + formData.append("gameId", raidGroup.gameId); + if(raidGroup.raidGroupIcon){ + formData.append("raidGroupIcon", raidGroup.raidGroupIcon); + } + + const response = await api.put(`/raidGroup/${raidGroup.raidGroupId}`, formData); + + if(response.status !== 200){ + throw new Error("Failed to update raid group"); + } + else if(response.data.errors){ + throw new Error(response.data.errors.join(", ")); + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["raidGroups"] }); + } + }); +} + +export function useDeleteRaidGroup(){ + const queryClient = useQueryClient(); + + + return useMutation({ + mutationKey: ["deleteRaidGroup"], + mutationFn: async (raidGroupId: string) => { + const response = await api.delete(`/raidGroup/${raidGroupId}`); + + if(response.status !== 200){ + throw new Error("Failed to delete raid group"); + } + else if(response.data.errors){ + throw new Error(response.data.errors.join(", ")); + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["raidGroups"] }); + } + }); +} diff --git a/src/interface/RaidGroup.ts b/src/interface/RaidGroup.ts new file mode 100644 index 0000000..b704046 --- /dev/null +++ b/src/interface/RaidGroup.ts @@ -0,0 +1,6 @@ +export interface RaidGroup { + raidGroupId?: string; + gameId: string; + raidGroupName: string; + raidGroupIcon?: string; +} diff --git a/src/pages/protected/AdminPage.tsx b/src/pages/protected/AdminPage.tsx index 9f09480..14f0afb 100644 --- a/src/pages/protected/AdminPage.tsx +++ b/src/pages/protected/AdminPage.tsx @@ -1,6 +1,7 @@ import TabGroup, { Tab } from "@/components/tab/TabGroup"; import AdminAccountsTab from "@/ui/account/AdminAccountsTab"; import AdminGamesTab from "@/ui/game/AdminGamesTab"; +import AdminRaidGroupTab from "@/ui/raidGroup/AdminRaidGroupTab"; export default function AdminPage(){ @@ -12,6 +13,10 @@ export default function AdminPage(){ { tabHeader: "Games", tabContent: + }, + { + tabHeader: "Raid Groups", + tabContent: } ]; diff --git a/src/ui/game/modals/DeleteGameModal.tsx b/src/ui/game/modals/DeleteGameModal.tsx index cb14316..d56d246 100644 --- a/src/ui/game/modals/DeleteGameModal.tsx +++ b/src/ui/game/modals/DeleteGameModal.tsx @@ -6,6 +6,7 @@ import { Game } from "@/interface/Game"; import { useTimedModal } from "@/providers/TimedModalProvider"; import { useEffect } from "react"; + export default function DeleteGameModal({ display, close, diff --git a/src/ui/raidGroup/AdminRaidGroupTab.tsx b/src/ui/raidGroup/AdminRaidGroupTab.tsx new file mode 100644 index 0000000..b1b3868 --- /dev/null +++ b/src/ui/raidGroup/AdminRaidGroupTab.tsx @@ -0,0 +1,96 @@ +import PrimaryButton from "@/components/button/PrimaryButton"; +import TextInput from "@/components/input/TextInput"; +import Pagination from "@/components/pagination/Pagination"; +import { useGetRaidGroupsCount } from "@/hooks/RaidGroupHooks"; +import { useEffect, useState } from "react"; +import { useDebouncedCallback } from "use-debounce"; +import RaidGroupModal from "./modals/RaidGroupModal"; +import RaidGroupsLoader from "./RaidGroupsLoader"; + +export default function AdminRaidGroupTab(){ + const [ displayCreateRaidGroupModal, setDisplayRaidGroupModal ] = useState(false); + const [ page, setPage ] = useState(1); + const [ totalPages, setTotalPages ] = useState(1); + const [ searchTerm, setSearchTerm ] = useState(""); + const [ sentSearchTerm, setSentSearchTerm ] = useState(); + const pageSize = 10; + const modalId = crypto.randomUUID().replace("-", ""); + + + const raidGroupsCountQuery = useGetRaidGroupsCount(sentSearchTerm); + + + const updateSearchTerm = useDebouncedCallback((newSearchTerm: string) => { + setSentSearchTerm(newSearchTerm.length ? newSearchTerm : undefined); + }, 1000); + + useEffect(() => { + updateSearchTerm(searchTerm); + }, [ searchTerm, updateSearchTerm ]); + + useEffect(() => { + if(raidGroupsCountQuery.status === "success"){ + setTotalPages(Math.ceil(raidGroupsCountQuery.data / pageSize)); + } + }, [ raidGroupsCountQuery ]); + + + return ( + <> +
+
+   +
+ {/* Add Raid Group Button */} +
+ setDisplayRaidGroupModal(true)} + > + Create Raid Group + + setDisplayRaidGroupModal(false)} + raidGroup={undefined} + /> +
+ {/* Raid Group Search Box */} +
+
+ setSearchTerm(e.target.value)} + placeholder="Search" + /> +
+
+
+ {/* Raid Group List */} + + {/* Pagination */} +
+ +
+ + ); +} diff --git a/src/ui/raidGroup/RaidGroupAdminButtons.tsx b/src/ui/raidGroup/RaidGroupAdminButtons.tsx new file mode 100644 index 0000000..59125aa --- /dev/null +++ b/src/ui/raidGroup/RaidGroupAdminButtons.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 RaidGroupAdminButtons({ + buttonProps, + showEditRaidGroupModal, + showDeleteRaidGroupModal +}:{ + buttonProps: ButtonProps; + showEditRaidGroupModal: () => void; + showDeleteRaidGroupModal: () => void; +}){ + return ( +
+ + + + + + +
+ ); +} diff --git a/src/ui/raidGroup/RaidGroupsList.tsx b/src/ui/raidGroup/RaidGroupsList.tsx new file mode 100644 index 0000000..112e25b --- /dev/null +++ b/src/ui/raidGroup/RaidGroupsList.tsx @@ -0,0 +1,102 @@ +import { ButtonProps } from "@/components/button/Button"; +import Table from "@/components/table/Table"; +import { RaidGroup } from "@/interface/RaidGroup"; +import { useState } from "react"; +import DeleteRaidGroupModal from "./modals/DeleteRaidGroupModal"; +import RaidGroupModal from "./modals/RaidGroupModal"; +import RaidGroupAdminButtons from "./RaidGroupAdminButtons"; + + +export default function RaidGroupsList({ + raidGroups +}:{ + raidGroups: RaidGroup[]; +}){ + const [ selectedRaidGroup, setSelectedRaidGroup ] = useState(); + const [ displayEditRaidGroupModal, setDisplayEditRaidGroupModal ] = useState(false); + const [ displayDeleteRaidGroupModal, setDisplayDeleteRaidGroupModal ] = useState(false); + + + const buttonProps: ButtonProps = { + variant: "ghost", + size: "md", + shape: "square" + }; + + + const headElements: React.ReactNode[] = [ +
+ Icon +
, +
+ Name +
, +
+ Actions +
+ ]; + + const bodyElements: React.ReactNode[][] = raidGroups.map((raidGroup) => [ +
+ { + raidGroup.raidGroupIcon && +
+ +
+ } +   +
, +
+ {raidGroup.raidGroupName} +
, +
+
+   +
+ { + setSelectedRaidGroup(raidGroup); + setDisplayEditRaidGroupModal(true); + }} + showDeleteRaidGroupModal={() => { + setSelectedRaidGroup(raidGroup); + setDisplayDeleteRaidGroupModal(true); + }} + /> +
+ ]); + + + return ( + <> + + {setDisplayEditRaidGroupModal(false); setSelectedRaidGroup(undefined);}} + raidGroup={selectedRaidGroup} + /> + {setDisplayDeleteRaidGroupModal(false); setSelectedRaidGroup(undefined);}} + raidGroup={selectedRaidGroup} + /> + + ); +} diff --git a/src/ui/raidGroup/RaidGroupsListSkeleton.tsx b/src/ui/raidGroup/RaidGroupsListSkeleton.tsx new file mode 100644 index 0000000..88e46c3 --- /dev/null +++ b/src/ui/raidGroup/RaidGroupsListSkeleton.tsx @@ -0,0 +1,7 @@ +export default function RaidGroupsListSkeleton(){ + return ( +
+ Raid Group List Skeleton +
+ ); +} diff --git a/src/ui/raidGroup/RaidGroupsLoader.tsx b/src/ui/raidGroup/RaidGroupsLoader.tsx new file mode 100644 index 0000000..eef08d0 --- /dev/null +++ b/src/ui/raidGroup/RaidGroupsLoader.tsx @@ -0,0 +1,31 @@ +import DangerMessage from "@/components/message/DangerMessage"; +import { useGetRaidGroups } from "@/hooks/RaidGroupHooks"; +import RaidGroupsList from "./RaidGroupsList"; +import RaidGroupsListSkeleton from "./RaidGroupsListSkeleton"; + +export default function RaidGroupsLoader({ + page, + pageSize, + searchTerm +}:{ + page: number; + pageSize: number; + searchTerm?: string; +}){ + const raidGroupsQuery = useGetRaidGroups(page - 1, pageSize, searchTerm); + + + if(raidGroupsQuery.status === "pending"){ + return + } + else if(raidGroupsQuery.status === "error"){ + return Error {raidGroupsQuery.error.message} + } + else{ + return ( + + ); + } +} diff --git a/src/ui/raidGroup/modals/DeleteRaidGroupModal.tsx b/src/ui/raidGroup/modals/DeleteRaidGroupModal.tsx new file mode 100644 index 0000000..1061210 --- /dev/null +++ b/src/ui/raidGroup/modals/DeleteRaidGroupModal.tsx @@ -0,0 +1,63 @@ +import DangerButton from "@/components/button/DangerButton"; +import SecondaryButton from "@/components/button/SecondaryButton"; +import RaidBuilderModal from "@/components/modal/RaidBuilderModal"; +import { useDeleteRaidGroup } from "@/hooks/RaidGroupHooks"; +import { RaidGroup } from "@/interface/RaidGroup"; +import { useTimedModal } from "@/providers/TimedModalProvider"; +import { useEffect } from "react"; + + +export default function DeleteRaidGroupModal({ + display, + close, + raidGroup +}:{ + display: boolean; + close: () => void; + raidGroup: RaidGroup | undefined; +}){ + const deleteRaidGroupMutate = useDeleteRaidGroup(); + const { addSuccessMessage, addErrorMessage } = useTimedModal(); + + + const deleteGame = () => { + deleteRaidGroupMutate.mutate(raidGroup?.raidGroupId ?? ""); + } + + useEffect(() => { + if(deleteRaidGroupMutate.status === "success"){ + deleteRaidGroupMutate.reset(); + addSuccessMessage(`Successfully delete ${raidGroup?.raidGroupName}`); + close(); + } + else if(deleteRaidGroupMutate.status === "error"){ + deleteRaidGroupMutate.reset(); + addErrorMessage(`Error deleting game ${raidGroup?.raidGroupName}: ${deleteRaidGroupMutate.error.message}`); + console.log(deleteRaidGroupMutate.error); + } + }); + + + return ( + + + Delete + + + Cancel + + + } + /> + ); +} diff --git a/src/ui/raidGroup/modals/RaidGroupModal.tsx b/src/ui/raidGroup/modals/RaidGroupModal.tsx new file mode 100644 index 0000000..5302a40 --- /dev/null +++ b/src/ui/raidGroup/modals/RaidGroupModal.tsx @@ -0,0 +1,130 @@ +import PrimaryButton from "@/components/button/PrimaryButton"; +import SecondaryButton from "@/components/button/SecondaryButton"; +import GameSelector from "@/components/game/GameSelector"; +import IconInput from "@/components/input/IconInput"; +import TextInput from "@/components/input/TextInput"; +import RaidBuilderModal from "@/components/modal/RaidBuilderModal"; +import { useGetGame } from "@/hooks/GameHooks"; +import { useCreateRaidGroup, useUpdateRaidGroup } from "@/hooks/RaidGroupHooks"; +import { Game } from "@/interface/Game"; +import { RaidGroup } from "@/interface/RaidGroup"; +import { useTimedModal } from "@/providers/TimedModalProvider"; +import { useEffect, useState } from "react"; + + +export default function RaidGroupModal({ + display, + close, + raidGroup +}:{ + display: boolean; + close: () => void; + raidGroup?: RaidGroup; +}){ + const [ raidGroupName, setRaidGroupName ] = useState(raidGroup?.raidGroupName); + const [ raidGroupIcon, setRaidGroupIcon ] = useState(raidGroup?.raidGroupIcon); + const [ iconFile, setIconFile ] = useState(null); + const [ game, setGame ] = useState(); + const modalId = crypto.randomUUID().replace("-", ""); + + + useEffect(() => { + setRaidGroupName(raidGroup?.raidGroupName ?? ""); + setRaidGroupIcon(raidGroup?.raidGroupIcon ?? ""); + }, [ raidGroup, setRaidGroupName, setRaidGroupIcon ]); + + + const updateRaidGroupMutate = useUpdateRaidGroup(); + const createRaidGroupMutate = useCreateRaidGroup(); + const { addSuccessMessage, addErrorMessage } = useTimedModal(); + const gameQuery = useGetGame(raidGroup?.gameId ?? "", raidGroup === undefined); + if(gameQuery.status === "success" && !game){ + setGame(gameQuery.data); + } + + + useEffect(() => { + if(updateRaidGroupMutate.status === "success"){ + updateRaidGroupMutate.reset(); + addSuccessMessage(`Updated raid group ${raidGroupName}`); + close(); + } + else if(updateRaidGroupMutate.status === "error"){ + updateRaidGroupMutate.reset(); + addErrorMessage(`Error updating raid group ${raidGroupName}: ${updateRaidGroupMutate.error.message}`); + console.log(updateRaidGroupMutate.error); + } + else if(createRaidGroupMutate.status === "success"){ + createRaidGroupMutate.reset(); + addSuccessMessage(`Created raid group ${raidGroupName}`); + close(); + } + else if(createRaidGroupMutate.status === "error"){ + createRaidGroupMutate.reset(); + addErrorMessage(`Error creating raid group ${raidGroupName}: ${createRaidGroupMutate.error.message}`); + console.log(createRaidGroupMutate.error); + } + }, [ updateRaidGroupMutate, createRaidGroupMutate, raidGroupName, close, addSuccessMessage, addErrorMessage ]); + + + const updateRaidGroup = () => { + updateRaidGroupMutate.mutate({raidGroup: {raidGroupId: raidGroup?.raidGroupId, raidGroupName, gameId: game?.gameId, raidGroupIcon} as RaidGroup, iconFile}); + } + + const createRaidGroup = () => { + createRaidGroupMutate.mutate({raidGroupName: raidGroupName ?? "", gameId: game?.gameId ?? "", iconFile}); + } + + + return ( + +
+ setRaidGroupName(e.target.value)} + /> +
+
+ +
+ {setIconFile(file); setRaidGroupIcon(undefined);}} + addErrorMessage={addErrorMessage} + /> + + } + modalFooter={ + <> + + {raidGroup ? "Update" : "Create"} + + + Cancel + + + } + /> + ); +}