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
+
+ >
+ }
+ />
+ );
+}