diff --git a/src/components/classGroup/ClassGroupSelector.tsx b/src/components/classGroup/ClassGroupSelector.tsx
new file mode 100644
index 0000000..067cd47
--- /dev/null
+++ b/src/components/classGroup/ClassGroupSelector.tsx
@@ -0,0 +1,68 @@
+import { ClassGroup } from "@/interface/ClassGroup";
+import SelectClassGroupModal from "@/ui/classGroup/modal/SelectClassGroupModal";
+import { useEffect, useState } from "react";
+
+
+export default function ClassGroupSelector({
+ raidGroupId,
+ selectedClassGroups,
+ onChange
+}:{
+ raidGroupId: string;
+ selectedClassGroups: (ClassGroup | null)[];
+ onChange: (classGroups: (ClassGroup | null)[]) => void;
+}){
+ const [ classGroups, setClassGroups ] = useState(selectedClassGroups);
+ const [ displaySelectClassGroupModal, setDisplaySelectClassGroupModal ] = useState(false);
+ const [ selectedCell, setSelectedCell ] = useState(0);
+
+
+ useEffect(() => {
+ setClassGroups(selectedClassGroups);
+ }, [selectedClassGroups]);
+
+
+ const updateClassGroups = (classGroup?: ClassGroup | null) => {
+ const newClassGroups = [...classGroups];
+ if(classGroup){
+ newClassGroups[selectedCell] = classGroup;
+ }
+ else{
+ newClassGroups[selectedCell] = null;
+ }
+ setClassGroups(newClassGroups);
+ onChange(newClassGroups);
+ }
+
+
+ return (
+ <>
+
+ {
+ classGroups.map((classGroup, index) => (
+
{
+ setDisplaySelectClassGroupModal(true);
+ setSelectedCell(index);
+ }}
+ >
+ {classGroup?.classGroupName ?? "Any"}
+
+ ))
+ }
+
+ setDisplaySelectClassGroupModal(false)}
+ selectedClassGroup={classGroups[selectedCell]}
+ onChange={updateClassGroups}
+ raidGroupId={raidGroupId}
+ />
+ >
+ );
+}
diff --git a/src/components/classGroup/ClassGroupsByRaidLayoutDisplay.tsx b/src/components/classGroup/ClassGroupsByRaidLayoutDisplay.tsx
new file mode 100644
index 0000000..59fc941
--- /dev/null
+++ b/src/components/classGroup/ClassGroupsByRaidLayoutDisplay.tsx
@@ -0,0 +1,41 @@
+import { useGetClassGroupsByRaidLayout } from "@/hooks/ClassGroupHooks";
+import DangerMessage from "../message/DangerMessage";
+
+export default function ClassGroupsByRaidLayoutDisplay({
+ raidGroupId,
+ raidLayoutId
+}:{
+ raidGroupId: string;
+ raidLayoutId: string;
+}){
+ const classGroupsQuery = useGetClassGroupsByRaidLayout(raidGroupId, raidLayoutId);
+
+
+ if(classGroupsQuery.status === "pending"){
+ return (Loading...
);
+ }
+ else if(classGroupsQuery.status === "error"){
+ return (Error: {classGroupsQuery.error.message});
+ }
+ else{
+ return (
+
+ {
+ classGroupsQuery.data.map((classGroup, index) => (
+
+ {classGroup?.classGroupName ?? "Any"}
+
+ ))
+ }
+ {
+ classGroupsQuery.data.length === 0 &&
+ <> >
+ }
+
+ );
+ }
+}
diff --git a/src/components/input/NumberInput.tsx b/src/components/input/NumberInput.tsx
new file mode 100644
index 0000000..0f29f3f
--- /dev/null
+++ b/src/components/input/NumberInput.tsx
@@ -0,0 +1,86 @@
+import clsx from "clsx";
+import { useEffect, useState } from "react";
+
+
+export default function NumberInput({
+ id,
+ name,
+ label,
+ defaultValue,
+ value,
+ min,
+ max,
+ onChange,
+ disabled
+}:{
+ id: string;
+ name?: string;
+ accepted?: boolean;
+ label?: string;
+ defaultValue?: number;
+ value?: number;
+ min?: number;
+ max?: number;
+ onChange?: (value: number) => void;
+ disabled?: boolean;
+}){
+ const [ inputValue, setInputValue ] = useState(value ?? 1);
+ const [ minValue, setMinValue ] = useState(min ?? Number.MIN_VALUE);
+ const [ maxValue, setMaxValue ] = useState(max ?? Number.MAX_VALUE);
+ const inputId = crypto.randomUUID().replaceAll("-", "");
+
+
+ useEffect(() => {
+ setMinValue(min ?? Number.MIN_VALUE);
+ setMaxValue(max ?? Number.MAX_VALUE);
+ setInputValue(value ?? defaultValue ?? 0);
+ }, [ id, min, max, defaultValue, value ]);
+
+
+ const changeInput = (value: number) => {
+ if(value < minValue){
+ value = maxValue;
+ }
+ else if(value > maxValue){
+ value = minValue;
+ }
+
+ setInputValue(value);
+ onChange?.(value);
+ }
+
+
+ return (
+
+
+ changeInput(parseInt(e.target.value || "1"))}
+ value={inputValue}
+ disabled={disabled}
+ />
+
+
+
+ );
+}
diff --git a/src/hooks/ClassGroupHooks.ts b/src/hooks/ClassGroupHooks.ts
index 1ac8389..ca7cbac 100644
--- a/src/hooks/ClassGroupHooks.ts
+++ b/src/hooks/ClassGroupHooks.ts
@@ -52,6 +52,25 @@ export function useGetClassGroupsCount(raidGroupId: string, searchTerm?: string)
});
}
+export function useGetClassGroupsByRaidLayout(raidGroupId: string, raidLayoutId: string | undefined){
+ return useQuery({
+ queryKey: ["classGroups", "raidLayout", raidLayoutId],
+ queryFn: async () => {
+ const response = await api.get(`/raidGroup/${raidGroupId}/classGroup/raidLayout/${raidLayoutId}`);
+
+ if(response.status !== 200){
+ throw new Error("Failed to get class groups");
+ }
+ else if(response.data.error){
+ throw new Error(response.data.errors.join(", "));
+ }
+
+ return response.data as (ClassGroup | null)[];
+ },
+ enabled: !!raidLayoutId
+ })
+}
+
export function useCreateClassGroup(raidGroupId: string){
const queryClient = useQueryClient();
diff --git a/src/hooks/RaidLayoutHooks.ts b/src/hooks/RaidLayoutHooks.ts
new file mode 100644
index 0000000..abccede
--- /dev/null
+++ b/src/hooks/RaidLayoutHooks.ts
@@ -0,0 +1,130 @@
+import { RaidLayout, RaidLayoutClassGroupXref } from "@/interface/RaidLayout";
+import { api } from "@/util/AxiosUtil";
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+
+
+export function useGetRaidLayoutsByRaidGroup(raidGroupId: string, page: number, pageSize: number, searchTerm?: string){
+ return useQuery({
+ queryKey: ["raidLayouts", raidGroupId, {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/${raidGroupId}/raidLayout?${params}`);
+
+ if(response.status !== 200){
+ throw new Error("Failed to get accounts");
+ }
+ else if(response.data.errors){
+ throw new Error(response.data.errors.join(", "));
+ }
+
+ return response.data as RaidLayout[];
+ }
+ });
+}
+
+export function useGetRaidLayoutsByRaidGroupCount(raidGroupId: string, searchTerm?: string){
+ return useQuery({
+ queryKey: ["raidLayouts", raidGroupId, "count", searchTerm],
+ queryFn: async () => {
+ const params = new URLSearchParams();
+ if(searchTerm){
+ params.append("searchTerm", searchTerm);
+ }
+
+ const response = await api.get(`/raidGroup/${raidGroupId}/raidLayout/count?${params}`);
+
+ if(response.status !== 200){
+ throw new Error("Failed to get accounts");
+ }
+ else if(response.data.errors){
+ throw new Error(response.data.errors.join(", "));
+ }
+
+ return response.data.count as number;
+ }
+ });
+}
+
+export function useCreateRaidLayout(raidGroupId: string){
+ const queryClient = useQueryClient();
+
+
+ return useMutation({
+ mutationKey: ["createRaidLayout", raidGroupId],
+ mutationFn: async ({raidLayout, raidLayoutClassGroupXrefs}:{raidLayout: RaidLayout; raidLayoutClassGroupXrefs: RaidLayoutClassGroupXref[];}) => {
+ const response = await api.post(`/raidGroup/${raidGroupId}/raidLayout`,
+ {
+ raidLayout,
+ raidLayoutClassGroupXrefs
+ }
+ );
+
+ if(response.status !== 200){
+ throw new Error("Failed to create raid layout");
+ }
+ else if(response.data.errors){
+ throw new Error(response.data.errors.join(", "));
+ }
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["raidLayouts", raidGroupId] });
+ }
+ });
+}
+
+export function useUpdateRaidLayout(raidGroupId: string){
+ const queryClient = useQueryClient();
+
+
+ return useMutation({
+ mutationKey: ["updateRaidLayout", raidGroupId],
+ mutationFn: async ({raidLayout, raidLayoutClassGroupXrefs}:{raidLayout: RaidLayout; raidLayoutClassGroupXrefs: RaidLayoutClassGroupXref[];}) => {
+ const response = await api.put(`/raidGroup/${raidGroupId}/raidLayout/${raidLayout.raidLayoutId}`,
+ {
+ raidLayout,
+ raidLayoutClassGroupXrefs
+ }
+ );
+
+ if(response.status !== 200){
+ throw new Error("Failed to update raid layout");
+ }
+ else if(response.data.errors){
+ throw new Error(response.data.errors.join(", "));
+ }
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["raidLayouts", raidGroupId] });
+ queryClient.invalidateQueries({ queryKey: ["classGroups", "raidLayout"] });
+ }
+ });
+}
+
+export function useDeleteRaidLayout(raidGroupId: string, raidLayoutId: string){
+ const queryClient = useQueryClient();
+
+
+ return useMutation({
+ mutationKey: ["deleteRaidLayout", raidGroupId, raidLayoutId],
+ mutationFn: async () => {
+ const response = await api.delete(`/raidGroup/${raidGroupId}/raidLayout/${raidLayoutId}`);
+
+ if(response.status !== 200){
+ throw new Error("Failed to delete raid layout");
+ }
+ else if(response.data.errors){
+ throw new Error(response.data.errors.join(", "));
+ }
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["raidLayouts", raidGroupId] });
+ }
+ });
+}
+
diff --git a/src/index.css b/src/index.css
index 00f1520..9e4d088 100644
--- a/src/index.css
+++ b/src/index.css
@@ -10,6 +10,7 @@
:root.dark {
--text-color: #FFFFFFDE;
--bg-color: var(--color-neutral-825);
+ color-scheme: dark;
}
:root.light {
diff --git a/src/interface/RaidLayout.ts b/src/interface/RaidLayout.ts
new file mode 100644
index 0000000..0284b41
--- /dev/null
+++ b/src/interface/RaidLayout.ts
@@ -0,0 +1,13 @@
+export interface RaidLayout {
+ raidLayoutId?: string;
+ raidGroupId: string;
+ raidLayoutName: string;
+ raidSize: number;
+}
+
+export interface RaidLayoutClassGroupXref {
+ raidLayoutClassGroupXrefId?: string;
+ raidLayoutId: string;
+ classGroupId?: string;
+ layoutLocation: number;
+}
diff --git a/src/pages/protected/RaidGroupPage.tsx b/src/pages/protected/RaidGroupPage.tsx
index 2899093..f11906f 100644
--- a/src/pages/protected/RaidGroupPage.tsx
+++ b/src/pages/protected/RaidGroupPage.tsx
@@ -5,6 +5,7 @@ import RaidGroupCalendarDisplay from "@/ui/calendar/RaidGroupCalendarDisplay";
import RaidGroupHeader from "@/ui/calendar/RaidGroupHeader";
import ClassGroupsTab from "@/ui/classGroup/ClassGroupsTab";
import PersonTab from "@/ui/person/PersonTab";
+import RaidLayoutTab from "@/ui/raidLayout/RaidLayoutTab";
import { useEffect, useState } from "react";
import { Navigate, useParams } from "react-router";
@@ -50,6 +51,10 @@ export default function RaidGroupPage(){
{
tabHeader: "Class Groups",
tabContent:
+ },
+ {
+ tabHeader: "Raid Layout",
+ tabContent:
}
];
diff --git a/src/ui/classGroup/modal/SelectClassGroupModal.tsx b/src/ui/classGroup/modal/SelectClassGroupModal.tsx
new file mode 100644
index 0000000..e3cd6e1
--- /dev/null
+++ b/src/ui/classGroup/modal/SelectClassGroupModal.tsx
@@ -0,0 +1,95 @@
+import PrimaryButton from "@/components/button/PrimaryButton";
+import SecondaryButton from "@/components/button/SecondaryButton";
+import DangerMessage from "@/components/message/DangerMessage";
+import RaidBuilderModal from "@/components/modal/RaidBuilderModal";
+import { useGetClassGroups } from "@/hooks/ClassGroupHooks";
+import { ClassGroup } from "@/interface/ClassGroup";
+import { useEffect, useState } from "react";
+
+
+export default function SelectClassGroupModal({
+ display,
+ close,
+ selectedClassGroup,
+ onChange,
+ raidGroupId
+}:{
+ display: boolean;
+ close: () => void;
+ selectedClassGroup?: ClassGroup | null;
+ onChange: (classGroup?: ClassGroup | null) => void;
+ raidGroupId: string;
+}){
+ const [ currentClassGroup, setCurrentClassGroup ] = useState(selectedClassGroup);
+ const selectorId = crypto.randomUUID().replaceAll("-", "");
+
+ const classGroupsQuery = useGetClassGroups(raidGroupId, 0, 100);
+
+
+ useEffect(() => {
+ setCurrentClassGroup(selectedClassGroup);
+ }, [ selectedClassGroup ]);
+
+
+ if(classGroupsQuery.status === "pending"){
+ return Loading...
+ }
+ else if(classGroupsQuery.status === "error"){
+ return Error: {classGroupsQuery.error.message}
+ }
+ else{
+ return (
+
+ {
+ classGroupsQuery.data?.map((classGroup) => (
+
+ setCurrentClassGroup(currentClassGroup === classGroup ? undefined : classGroup)}
+ onChange={() => {}}
+ />
+
+
+ ))
+ }
+
+ }
+ modalFooter={
+ <>
+ { onChange(currentClassGroup); close(); }}
+ >
+ Select
+
+
+ Cancel
+
+ >
+ }
+ />
+ );
+ }
+}
diff --git a/src/ui/raidLayout/RaidLayoutAdminButtons.tsx b/src/ui/raidLayout/RaidLayoutAdminButtons.tsx
new file mode 100644
index 0000000..a247ee1
--- /dev/null
+++ b/src/ui/raidLayout/RaidLayoutAdminButtons.tsx
@@ -0,0 +1,37 @@
+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 RaidLayoutAdminButtons({
+ buttonProps,
+ showRaidLayoutModal,
+ showDeleteRaidLayoutModal
+}:{
+ buttonProps: ButtonProps;
+ showRaidLayoutModal: () => void;
+ showDeleteRaidLayoutModal: () => void;
+}){
+ return (
+
+ );
+}
diff --git a/src/ui/raidLayout/RaidLayoutList.tsx b/src/ui/raidLayout/RaidLayoutList.tsx
new file mode 100644
index 0000000..1528c3a
--- /dev/null
+++ b/src/ui/raidLayout/RaidLayoutList.tsx
@@ -0,0 +1,109 @@
+import { ButtonProps } from "@/components/button/Button";
+import ClassGroupsByRaidLayoutDisplay from "@/components/classGroup/ClassGroupsByRaidLayoutDisplay";
+import Table from "@/components/table/Table";
+import { useGetClassGroupsByRaidLayout } from "@/hooks/ClassGroupHooks";
+import { ClassGroup } from "@/interface/ClassGroup";
+import { RaidGroup } from "@/interface/RaidGroup";
+import { RaidLayout } from "@/interface/RaidLayout";
+import { useEffect, useState } from "react";
+import DeleteRaidLayoutModal from "./modal/DeleteRaidLayoutModal";
+import RaidLayoutModal from "./modal/RaidLayoutModal";
+import RaidLayoutAdminButtons from "./RaidLayoutAdminButtons";
+
+
+export default function RaidLayoutList({
+ raidLayouts,
+ raidGroup
+}:{
+ raidLayouts: RaidLayout[];
+ raidGroup: RaidGroup;
+}){
+ const [ selectedRaidLayout, setSelectedRaidLayout ] = useState();
+ const [ displayEditRaidLayoutModal, showEditRaidLayoutModal ] = useState(false);
+ const [ displayDeleteRaidLayoutModal, showDeleteRaidLayoutModal ] = useState(false);
+ const [ selectedLayoutClassGroups, setSelectedLayoutClassGroups ] = useState<(ClassGroup | null)[]>([]);
+
+ const classGroupsQuery = useGetClassGroupsByRaidLayout(raidGroup.raidGroupId ?? "", selectedRaidLayout?.raidLayoutId);
+ useEffect(() => {
+ setSelectedLayoutClassGroups(classGroupsQuery.data ?? []);
+ }, [ classGroupsQuery.data ]);
+
+
+ const buttonProps: ButtonProps = {
+ variant: "ghost",
+ size: "md",
+ shape: "square"
+ };
+
+
+ const headElements: React.ReactNode[] = [
+
+ Raid Layout Name
+
,
+
+ Raid Size
+
,
+
+ Class Groups
+
,
+
+ Actions
+
+ ];
+
+ const bodyElements: React.ReactNode[][] = raidLayouts.map((raidLayout) => [
+
+ {raidLayout.raidLayoutName}
+
,
+
+ {raidLayout.raidSize}
+
,
+
+
+
,
+
+
+
+
+
{ setSelectedRaidLayout(raidLayout); showEditRaidLayoutModal(true); }}
+ showDeleteRaidLayoutModal={() => { setSelectedRaidLayout(raidLayout); showDeleteRaidLayoutModal(true); }}
+ />
+
+ ]);
+
+
+ return (
+ <>
+
+ { showEditRaidLayoutModal(false); setSelectedRaidLayout(undefined); }}
+ raidLayout={selectedRaidLayout}
+ raidGroup={raidGroup}
+ selectedClassGroups={selectedLayoutClassGroups}
+ />
+ { showDeleteRaidLayoutModal(false); setSelectedRaidLayout(undefined); }}
+ raidLayout={selectedRaidLayout}
+ />
+ >
+ );
+}
diff --git a/src/ui/raidLayout/RaidLayoutLoader.tsx b/src/ui/raidLayout/RaidLayoutLoader.tsx
new file mode 100644
index 0000000..f34b1bb
--- /dev/null
+++ b/src/ui/raidLayout/RaidLayoutLoader.tsx
@@ -0,0 +1,35 @@
+import DangerMessage from "@/components/message/DangerMessage";
+import { useGetRaidLayoutsByRaidGroup } from "@/hooks/RaidLayoutHooks";
+import { RaidGroup } from "@/interface/RaidGroup";
+import RaidLayoutList from "./RaidLayoutList";
+
+
+export default function RaidLayoutLoader({
+ page,
+ pageSize,
+ searchTerm,
+ raidGroup
+}:{
+ page: number;
+ pageSize: number;
+ searchTerm?: string;
+ raidGroup: RaidGroup;
+}){
+ const raidLayoutsQuery = useGetRaidLayoutsByRaidGroup(raidGroup.raidGroupId ?? "", page - 1, pageSize, searchTerm);
+
+
+ if(raidLayoutsQuery.status === "pending"){
+ return Loading...
+ }
+ else if(raidLayoutsQuery.status === "error"){
+ return Error: {raidLayoutsQuery.error.message}
+ }
+ else{
+ return (
+
+ );
+ }
+}
diff --git a/src/ui/raidLayout/RaidLayoutTab.tsx b/src/ui/raidLayout/RaidLayoutTab.tsx
new file mode 100644
index 0000000..e3dd00c
--- /dev/null
+++ b/src/ui/raidLayout/RaidLayoutTab.tsx
@@ -0,0 +1,106 @@
+import PrimaryButton from "@/components/button/PrimaryButton";
+import TextInput from "@/components/input/TextInput";
+import Pagination from "@/components/pagination/Pagination";
+import { useGetRaidLayoutsByRaidGroupCount } from "@/hooks/RaidLayoutHooks";
+import { RaidGroup } from "@/interface/RaidGroup";
+import { useEffect, useState } from "react";
+import { useDebouncedCallback } from "use-debounce";
+import RaidLayoutModal from "./modal/RaidLayoutModal";
+import RaidLayoutLoader from "./RaidLayoutLoader";
+
+
+export default function RaidLayoutTab({
+ raidGroup
+}:{
+ raidGroup: RaidGroup;
+}){
+ const [ displayCreateRaidLayoutModal, setDisplayCreateRaidLayoutModal ] = 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().replaceAll("-", "");
+
+
+ const raidLayoutCountQuery = useGetRaidLayoutsByRaidGroupCount(raidGroup.raidGroupId!, sentSearchTerm);
+
+
+ const updateSearchTerm = useDebouncedCallback((newSearchTerm: string) => {
+ setSentSearchTerm(newSearchTerm);
+ }, 1000);
+
+
+ useEffect(() => {
+ updateSearchTerm(searchTerm);
+ }, [ searchTerm, updateSearchTerm ]);
+
+ useEffect(() => {
+ if(raidLayoutCountQuery.status === "success"){
+ setTotalPages(Math.ceil(raidLayoutCountQuery.data / pageSize));
+ }
+ }, [ raidLayoutCountQuery ]);
+
+
+ return (
+ <>
+
+
+
+
+ {/* Add Raid Layout Button */}
+
+
setDisplayCreateRaidLayoutModal(true)}
+ >
+ Create Raid Layout
+
+
setDisplayCreateRaidLayoutModal(false)}
+ raidLayout={undefined}
+ raidGroup={raidGroup}
+ selectedClassGroups={[]}
+ />
+
+ {/* Raid Layout Search Box */}
+
+
+ setSearchTerm(e.target.value)}
+ placeholder="Search"
+ />
+
+
+
+ {/* Raid Layout List */}
+
+ {/* Pagination */}
+
+ >
+ );
+}
diff --git a/src/ui/raidLayout/modal/DeleteRaidLayoutModal.tsx b/src/ui/raidLayout/modal/DeleteRaidLayoutModal.tsx
new file mode 100644
index 0000000..38f7e26
--- /dev/null
+++ b/src/ui/raidLayout/modal/DeleteRaidLayoutModal.tsx
@@ -0,0 +1,64 @@
+import DangerButton from "@/components/button/DangerButton";
+import SecondaryButton from "@/components/button/SecondaryButton";
+import RaidBuilderModal from "@/components/modal/RaidBuilderModal";
+import { useDeleteRaidLayout } from "@/hooks/RaidLayoutHooks";
+import { RaidLayout } from "@/interface/RaidLayout";
+import { useTimedModal } from "@/providers/TimedModalProvider";
+import { useEffect } from "react";
+
+
+export default function DeleteRaidLayoutModal({
+ display,
+ close,
+ raidLayout
+}:{
+ display: boolean;
+ close: () => void;
+ raidLayout?: RaidLayout;
+}){
+ const deleteRaidLayoutMutate = useDeleteRaidLayout(raidLayout?.raidGroupId ?? "", raidLayout?.raidLayoutId ?? "");
+ const { addSuccessMessage, addErrorMessage } = useTimedModal();
+
+
+ const deleteRaidLayout = () => {
+ deleteRaidLayoutMutate.mutate();
+ }
+
+
+ useEffect(() => {
+ if(deleteRaidLayoutMutate.status === "success"){
+ deleteRaidLayoutMutate.reset();
+ addSuccessMessage("Raid Layout Deleted");
+ close();
+ }
+ else if(deleteRaidLayoutMutate.status === "error"){
+ deleteRaidLayoutMutate.reset();
+ addErrorMessage(`Error deleting raid layout ${raidLayout?.raidLayoutName}: ${deleteRaidLayoutMutate.error.message}`);
+ console.log(deleteRaidLayoutMutate.error);
+ }
+ });
+
+
+ return (
+
+
+ Delete
+
+
+ Cancel
+
+ >
+ }
+ />
+ );
+}
diff --git a/src/ui/raidLayout/modal/RaidLayoutModal.tsx b/src/ui/raidLayout/modal/RaidLayoutModal.tsx
new file mode 100644
index 0000000..9236493
--- /dev/null
+++ b/src/ui/raidLayout/modal/RaidLayoutModal.tsx
@@ -0,0 +1,164 @@
+import PrimaryButton from "@/components/button/PrimaryButton";
+import SecondaryButton from "@/components/button/SecondaryButton";
+import ClassGroupSelector from "@/components/classGroup/ClassGroupSelector";
+import NumberInput from "@/components/input/NumberInput";
+import TextInput from "@/components/input/TextInput";
+import RaidBuilderModal from "@/components/modal/RaidBuilderModal";
+import { useCreateRaidLayout, useUpdateRaidLayout } from "@/hooks/RaidLayoutHooks";
+import { ClassGroup } from "@/interface/ClassGroup";
+import { RaidGroup } from "@/interface/RaidGroup";
+import { RaidLayout, RaidLayoutClassGroupXref } from "@/interface/RaidLayout";
+import { useTimedModal } from "@/providers/TimedModalProvider";
+import { useEffect, useState } from "react";
+
+
+export default function RaidLayoutModal({
+ display,
+ close,
+ raidLayout,
+ raidGroup,
+ selectedClassGroups
+}:{
+ display: boolean;
+ close: () => void;
+ raidLayout?: RaidLayout;
+ raidGroup: RaidGroup;
+ selectedClassGroups: (ClassGroup | null)[];
+}){
+ const [ raidLayoutName, setRaidLayoutName ] = useState(raidLayout?.raidLayoutName ?? "");
+ const [ raidLayoutSize, setRaidLayoutSize ] = useState(raidLayout?.raidSize ?? 0);
+ const [ classGroups, setClassGroups ] = useState(selectedClassGroups ?? []);
+ const modalId = crypto.randomUUID().replaceAll("-", "");
+
+
+ useEffect(() => {
+ setRaidLayoutName(raidLayout?.raidLayoutName ?? "");
+ setRaidLayoutSize(raidLayout?.raidSize ?? 0);
+ setClassGroups(selectedClassGroups);
+ }, [ raidLayout, selectedClassGroups ]);
+
+ const updateRaidLayoutSize = (newLayoutSize: number) => {
+ console.log("newLayoutSize = " + newLayoutSize + ", " + classGroups.length);
+ setRaidLayoutSize(newLayoutSize);
+ if(newLayoutSize < classGroups.length){
+ console.log("Removing");
+ setClassGroups(classGroups.slice(0, newLayoutSize));
+ }
+ else if(newLayoutSize > classGroups.length){
+ console.log("Adding");
+ setClassGroups([
+ ...classGroups,
+ null
+ ]);
+ }
+ }
+ console.log("classGroups");
+ console.log(classGroups);
+
+
+ const createRaidLayoutMutate = useCreateRaidLayout(raidGroup.raidGroupId ?? "");
+ const updateRaidLayoutMutate = useUpdateRaidLayout(raidGroup.raidGroupId ?? "");
+ const { addSuccessMessage, addErrorMessage } = useTimedModal();
+
+
+ useEffect(() => {
+ if(createRaidLayoutMutate.status === "success"){
+ createRaidLayoutMutate.reset();
+ addSuccessMessage("Raid Layout Created");
+ close();
+ }
+ else if(createRaidLayoutMutate.status === "error"){
+ createRaidLayoutMutate.reset();
+ addErrorMessage(`Error creating raid layout ${raidLayoutName}: ${createRaidLayoutMutate.error.message}`);
+ console.log(createRaidLayoutMutate.error);
+ }
+ else if(updateRaidLayoutMutate.status === "success"){
+ updateRaidLayoutMutate.reset();
+ addSuccessMessage("Raid Layout Updated");
+ close();
+ }
+ else if(updateRaidLayoutMutate.status === "error"){
+ updateRaidLayoutMutate.reset();
+ addErrorMessage(`Error updating raid layout ${raidLayoutName}: ${updateRaidLayoutMutate.error.message}`);
+ console.log(updateRaidLayoutMutate.error);
+ }
+ });
+
+ const createRaidLayout = () => {
+ createRaidLayoutMutate.mutate({
+ raidLayout: {
+ raidLayoutName,
+ raidSize: raidLayoutSize,
+ raidGroupId: raidGroup.raidGroupId ?? ""
+ },
+ raidLayoutClassGroupXrefs: classGroups.map((cg, index) => ({
+ raidLayoutId: raidLayout?.raidLayoutId ?? "",
+ classGroupId: cg?.classGroupId,
+ layoutLocation: index
+ } as RaidLayoutClassGroupXref))
+ });
+ }
+
+ const updateRaidLayout = () => {
+ updateRaidLayoutMutate.mutate({
+ raidLayout: {
+ raidLayoutId: raidLayout?.raidLayoutId,
+ raidLayoutName,
+ raidSize: raidLayoutSize,
+ raidGroupId: raidGroup.raidGroupId ?? ""
+ },
+ raidLayoutClassGroupXrefs: classGroups.map((cg, index) => ({
+ raidLayoutId: raidLayout?.raidLayoutId ?? "",
+ classGroupId: cg?.classGroupId,
+ layoutLocation: index
+ } as RaidLayoutClassGroupXref))
+ });
+ }
+
+
+ return (
+
+ setRaidLayoutName(e.target.value)}
+ />
+
+
+
+ }
+ modalFooter={
+ <>
+
+ {raidLayout ? "Update" : "Create"}
+
+
+ Cancel
+
+ >
+ }
+ />
+ );
+}