diff --git a/src/components/input/TextInput.tsx b/src/components/input/TextInput.tsx
index 0ece7fb..0aa5f70 100644
--- a/src/components/input/TextInput.tsx
+++ b/src/components/input/TextInput.tsx
@@ -11,8 +11,12 @@ interface TextInputProps extends ComponentProps<"input">{
}
-export default function TextInput(props: TextInputProps){
+export default function TextInput(inProps: TextInputProps){
+ const props = {...inProps};
const { id, placeholder, name, inputClasses, labelClasses, accepted } = props;
+ delete props.inputClasses;
+ delete props.labelClasses;
+ delete props.accepted;
return (
diff --git a/src/components/personCharacter/PersonCharacterSelector.tsx b/src/components/personCharacter/PersonCharacterSelector.tsx
new file mode 100644
index 0000000..158d9ee
--- /dev/null
+++ b/src/components/personCharacter/PersonCharacterSelector.tsx
@@ -0,0 +1,76 @@
+import { PersonCharacter } from "@/interface/PersonCharacter";
+import { useEffect, useState } from "react";
+
+
+export default function PersonCharacterSelector({
+ personCharacters,
+ selectedCharacterId,
+ onChange
+}:{
+ personCharacters: PersonCharacter[];
+ selectedCharacterId?: string;
+ onChange?: (characterId: string | undefined) => void;
+}){
+ const [ currentlySelectedCharacterId, setCurrentlySelectedCharacterId ] = useState(selectedCharacterId);
+ const selectorId = crypto.randomUUID().replaceAll("-", "");
+
+
+ useEffect(() => {
+ setCurrentlySelectedCharacterId(selectedCharacterId);
+ }, [ selectedCharacterId ]);
+
+
+ const updateInput = (newCharacterId?: string) => {
+ if(newCharacterId === currentlySelectedCharacterId){
+ setCurrentlySelectedCharacterId(undefined);
+ onChange?.(undefined);
+ }
+ else{
+ setCurrentlySelectedCharacterId(newCharacterId);
+ onChange?.(newCharacterId);
+ }
+ }
+
+
+ return (
+
+ {
+ personCharacters.map((ch) => (
+
+
{}}
+ onClick={() => updateInput(ch.personCharacterId)}
+ />
+
+
+ ))
+ }
+
+ );
+}
diff --git a/src/components/personCharacter/RatingSelector.tsx b/src/components/personCharacter/RatingSelector.tsx
index fd8d8a4..9cab336 100644
--- a/src/components/personCharacter/RatingSelector.tsx
+++ b/src/components/personCharacter/RatingSelector.tsx
@@ -1,4 +1,5 @@
import { useEffect, useState } from "react";
+import NumberInput from "../input/NumberInput";
export default function RatingSelector({
rating,
@@ -8,6 +9,7 @@ export default function RatingSelector({
onChange?: (rating?: number) => void;
}){
const ratings = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
+ const selectorId = crypto.randomUUID().replaceAll("-", "");
const [ currentRating, setCurrentRating ] = useState(rating);
@@ -20,6 +22,19 @@ export default function RatingSelector({
}, [ currentRating, onChange ]);
+ return (
+
+ setCurrentRating(value)}
+ min={0}
+ max={10}
+ />
+
+ );
+
return (
+ }
+ modalFooter={
+ <>
+ {onSubmit(currentlySelectedPersonIds); close();}}
+ >
+ Select
+
+
+ Cancel
+
+ >
+ }
+ />
+ );
+}
diff --git a/src/ui/personCharacter/modal/PersonCharacterSelectorModal.tsx b/src/ui/personCharacter/modal/PersonCharacterSelectorModal.tsx
new file mode 100644
index 0000000..00e33af
--- /dev/null
+++ b/src/ui/personCharacter/modal/PersonCharacterSelectorModal.tsx
@@ -0,0 +1,177 @@
+import PrimaryButton from "@/components/button/PrimaryButton";
+import SecondaryButton from "@/components/button/SecondaryButton";
+import TextInput from "@/components/input/TextInput";
+import RaidBuilderModal from "@/components/modal/RaidBuilderModal";
+import Pagination from "@/components/pagination/Pagination";
+import PersonCharacterSelector from "@/components/personCharacter/PersonCharacterSelector";
+import { useGetGameClassesByClassGroup } from "@/hooks/GameClassHooks";
+import { ClassGroup } from "@/interface/ClassGroup";
+import { PersonCharacter } from "@/interface/PersonCharacter";
+import { getCharactersThatMatchClassGroup, getCharactersThatNotMatchClassGroup } from "@/util/PersonCharacterUtil";
+import clsx from "clsx";
+import { useEffect, useState } from "react";
+
+
+export default function PersonCharacterSelectorModal({
+ display,
+ close,
+ currentSlotClassGroup,
+ currentRunCharacters,
+ otherRunsCharacters,
+ personCharacters,
+ selectedCharacterId,
+ setInput
+}:{
+ display: boolean;
+ close: () => void;
+ currentSlotClassGroup?: ClassGroup;
+ currentRunCharacters: PersonCharacter[];
+ otherRunsCharacters: PersonCharacter[];
+ personCharacters: PersonCharacter[];
+ selectedCharacterId?: string;
+ setInput: (characterId?: string) => void;
+}){
+ enum SelectorTabs {
+ UNLOCKED = "UNLOCKED",
+ LOCKED = "LOCKED",
+ NO_MATCH = "NO_MATCH"
+ };
+
+
+ const [ page, setPage ] = useState(1);
+ const [ searchTerm, setSearchTerm ] = useState("");
+ const [ selectedTab, setSelectedTab ] = useState(SelectorTabs.UNLOCKED);
+ const [ matchingCharacters, setMatchingCharacters ] = useState(personCharacters);
+ const [ currentlyVisibleCharacters, setCurrentlyVisibleCharacters ] = useState([]);
+ const [ currentlySelectedCharacterId, setCurrentlySelectedCharacterId ] = useState(selectedCharacterId);
+ const pageSize = 9;
+ const modalId = crypto.randomUUID().replaceAll("-", "");
+
+
+ const gameClassQuery = useGetGameClassesByClassGroup(currentSlotClassGroup?.classGroupId);
+
+
+ //Update selected when the modal is updated
+ useEffect(() => {
+ setCurrentlySelectedCharacterId(selectedCharacterId);
+ }, [ selectedCharacterId ]);
+
+
+ //Update page data when modal becomes visible
+ useEffect(() => {
+ setPage(1);
+ setSearchTerm("");
+ setSelectedTab(SelectorTabs.UNLOCKED);
+ }, [ display, SelectorTabs.UNLOCKED ]);
+
+
+ //Update visible characters whenever page data changes
+ useEffect(() => {
+ //! Update visible characters
+
+ //Get characters that match the search term
+ let newMatchingCharacters = personCharacters.filter((ch) => ch.characterName.toLowerCase().includes(searchTerm?.toLowerCase() ?? ""));
+
+ if(selectedTab === SelectorTabs.UNLOCKED){
+ //Remove characters that don't belong to the current class group
+ newMatchingCharacters = getCharactersThatMatchClassGroup(newMatchingCharacters, gameClassQuery.status === "success" ? gameClassQuery.data : []);
+ //Remove characters that are locked in other runs
+ newMatchingCharacters = newMatchingCharacters.filter((ch) => !otherRunsCharacters.includes(ch));
+ }
+ else if(selectedTab === SelectorTabs.LOCKED){
+ //Remove characters that don't belong to the current class group
+ newMatchingCharacters = getCharactersThatMatchClassGroup(newMatchingCharacters, gameClassQuery.status === "success" ? gameClassQuery.data : []);
+ //Remove characters that aren't locked in other runs
+ newMatchingCharacters = newMatchingCharacters.filter((ch) => otherRunsCharacters.includes(ch));
+ }
+ else if(selectedTab === SelectorTabs.NO_MATCH){
+ //Remove characters that match the class group
+ const validMatches = getCharactersThatNotMatchClassGroup(newMatchingCharacters, gameClassQuery.status === "success" ? gameClassQuery.data : []);
+ newMatchingCharacters = newMatchingCharacters.filter((ch) => validMatches.includes(ch));
+ }
+
+ //Remove characters that belong to players that have other characters in this run, minus the current person
+ const invalidPersonIds = currentRunCharacters.filter((ch) => ch.personCharacterId !== selectedCharacterId).map((ch) => ch.personId);
+ newMatchingCharacters = newMatchingCharacters.filter((ch) => !invalidPersonIds.includes(ch.personId));
+
+ setMatchingCharacters(newMatchingCharacters);
+ //Add the currently selected character to the front of the array
+ //Apply paging
+ setCurrentlyVisibleCharacters(newMatchingCharacters.slice((page - 1) * pageSize, page * pageSize));
+ }, [ page, searchTerm, selectedTab, personCharacters, currentSlotClassGroup, SelectorTabs.UNLOCKED, SelectorTabs.LOCKED, SelectorTabs.NO_MATCH, currentRunCharacters, otherRunsCharacters, selectedCharacterId, gameClassQuery.status, gameClassQuery.data ]);
+
+
+ return (
+
+ {/* Search */}
+
+ setSearchTerm(e.target.value)}
+ />
+
+ {/* Tabs */}
+
+ {
+ Object.values(SelectorTabs).map((tab) => (
+
setSelectedTab(tab)}
+ >
+ {tab}
+
+ ))
+ }
+
+ {/* Character Selector */}
+
+ {/* Pagination */}
+
+ >
+ }
+ modalFooter={
+ <>
+ { setInput(currentlySelectedCharacterId); close(); }}
+ >
+ Select
+
+
+ Close
+
+ >
+ }
+ />
+ );
+}
diff --git a/src/ui/raidInstances/RaidInstanceAdminButtons.tsx b/src/ui/raidInstance/RaidInstanceAdminButtons.tsx
similarity index 100%
rename from src/ui/raidInstances/RaidInstanceAdminButtons.tsx
rename to src/ui/raidInstance/RaidInstanceAdminButtons.tsx
diff --git a/src/ui/raidInstance/RaidInstanceHeader.tsx b/src/ui/raidInstance/RaidInstanceHeader.tsx
new file mode 100644
index 0000000..72da72f
--- /dev/null
+++ b/src/ui/raidInstance/RaidInstanceHeader.tsx
@@ -0,0 +1,207 @@
+import PrimaryButton from "@/components/button/PrimaryButton";
+import DateInput from "@/components/input/DateInput";
+import NumberInput from "@/components/input/NumberInput";
+import TextInput from "@/components/input/TextInput";
+import { useUpdateRaidInstanceNoInvalidation, useUpdateRaidInstancePersonCharacterXrefs } from "@/hooks/RaidInstanceHooks";
+import { RaidInstance } from "@/interface/RaidInstance";
+import { useRaidInstanceContext } from "@/providers/RaidInstanceLayoutProvider";
+import { useTimedModal } from "@/providers/TimedModalProvider";
+import clsx from "clsx";
+import moment from "moment";
+import { useEffect, useState } from "react";
+import PersonSelectorModal from "../person/modals/PersonSelectorModal";
+import RaidLayoutSelectorModal from "../raidLayout/modal/RaidLayoutSelectorModal";
+
+
+export default function RaidInstanceHeader(){
+ const {
+ raidInstance, setRaidInstance,
+ raidGroup,
+ selectedClassGroups, setSelectedClassGroups,
+ raidLayout, setRaidLayout,
+ raidLayouts,
+ people,
+ roster, setRoster,
+ personCharacterXrefs, setPersonCharacterXrefs
+ } = useRaidInstanceContext();
+
+ const [ displayRaidLayoutSelectorModal, setDisplayRaidLayoutSelectorModal ] = useState(false);
+ const [ displayRosterSelectorModal, setDisplayRosterSelectorModal ] = useState(false);
+ const { addSuccessMessage, addErrorMessage } = useTimedModal();
+
+
+ const updateRaidLayout = (newRaidLayoutId?: string) => {
+ const newRaidLayout = raidLayouts.find((rl) => rl.raidLayoutId === newRaidLayoutId);
+
+ setRaidInstance({...raidInstance, raidLayoutId: newRaidLayoutId, raidSize: newRaidLayout?.raidSize ?? raidInstance?.raidSize} as RaidInstance);
+ setRaidLayout(newRaidLayout);
+ }
+
+ const updateRaidSize = (newRaidSize: number) => {
+ if(newRaidSize > (raidInstance?.raidSize ?? 0)){
+ setSelectedClassGroups([...selectedClassGroups, null]);
+ }
+ else{
+ setSelectedClassGroups(selectedClassGroups.slice(0, newRaidSize));
+ }
+ const newXrefs = personCharacterXrefs.filter((xref) => xref.positionNumber < newRaidSize);
+ setPersonCharacterXrefs(newXrefs);
+ setRaidInstance({...raidInstance, raidSize: newRaidSize, raidLayoutId: undefined} as RaidInstance);
+ setRaidLayout(undefined);
+ }
+
+ //Mutations
+ const { mutate: updateRaidInstanceMutate, status: updateRaidInstanceStatus, reset: updateRaidInstanceReset, error: updateRaidInstanceError } = useUpdateRaidInstanceNoInvalidation(raidGroup?.raidGroupId ?? "");
+ const { mutate: updatePersonCharacterXrefsMutate, status: updatePersonCharacterXrefsStatus, reset: updatePersonCharacterXrefsReset, error: updatePersonCharacterXrefsError } = useUpdateRaidInstancePersonCharacterXrefs(raidGroup?.raidGroupId ?? "", raidInstance?.raidInstanceId ?? "");
+
+ const saveRaidInstance = () => {
+ updateRaidInstanceMutate(raidInstance!);
+ updatePersonCharacterXrefsMutate(personCharacterXrefs);
+ }
+
+ useEffect(() => {
+ if((updateRaidInstanceStatus === "success") && (updatePersonCharacterXrefsStatus === "success")){
+ addSuccessMessage("Raid Instance Saved");
+ updateRaidInstanceReset();
+ updatePersonCharacterXrefsReset();
+ }
+ else if(updateRaidInstanceStatus === "error"){
+ addErrorMessage("Error Saving Raid Instance: " + updateRaidInstanceError.message);
+ updateRaidInstanceReset();
+ }
+ else if(updatePersonCharacterXrefsStatus === "error"){
+ addErrorMessage("Error Saving Raid Instance: " + updatePersonCharacterXrefsError.message);
+ updatePersonCharacterXrefsReset();
+ }
+ }, [ addErrorMessage, addSuccessMessage, updatePersonCharacterXrefsError?.message, updatePersonCharacterXrefsReset, updatePersonCharacterXrefsStatus, updateRaidInstanceError?.message, updateRaidInstanceReset, updateRaidInstanceStatus ]);
+
+
+ return (
+
+ {/* Raid Instance Name */}
+
+
+
+ setRaidInstance({
+ ...raidInstance,
+ raidInstanceName: e.target.value
+ } as RaidInstance)
+ }
+ //disabled={}
+ />
+
+
+ {/* Start and End Dates */}
+
+ setRaidInstance({...raidInstance, raidStartDate: moment(e.target.value).toDate()} as RaidInstance)}
+ />
+ setRaidInstance({...raidInstance, raidEndDate: moment(e.target.value).toDate()} as RaidInstance)}
+ />
+
+ {/* Raid Size & Layout */}
+
+ {/* Raid Size */}
+
+
+
+ {/* Raid Layout */}
+
+
setDisplayRaidLayoutSelectorModal(true)}
+ >
+
+
+ {raidLayout?.raidLayoutId ? raidLayout.raidLayoutName : "Select Raid Layout"}
+
+
+ Raid Layout
+
+
+
+
setDisplayRaidLayoutSelectorModal(false)}
+ onSubmit={updateRaidLayout}
+ />
+
+ {/* Roster */}
+
+
setDisplayRosterSelectorModal(true)}
+ >
+ Roster
+
+
setDisplayRosterSelectorModal(false)}
+ onSubmit={(newRosterIds) => setRoster(people.filter(person => newRosterIds.includes(person.personId ?? "")))}
+ people={people}
+ selectedPersonIds={roster.map(person => person.personId ?? "")}
+ />
+
+ {/* Save Button */}
+
+
+
+ );
+}
diff --git a/src/ui/raidInstances/RaidInstanceList.tsx b/src/ui/raidInstance/RaidInstanceList.tsx
similarity index 100%
rename from src/ui/raidInstances/RaidInstanceList.tsx
rename to src/ui/raidInstance/RaidInstanceList.tsx
diff --git a/src/ui/raidInstances/RaidInstanceListSkeleton.tsx b/src/ui/raidInstance/RaidInstanceListSkeleton.tsx
similarity index 100%
rename from src/ui/raidInstances/RaidInstanceListSkeleton.tsx
rename to src/ui/raidInstance/RaidInstanceListSkeleton.tsx
diff --git a/src/ui/raidInstances/RaidInstanceLoader.tsx b/src/ui/raidInstance/RaidInstanceLoader.tsx
similarity index 100%
rename from src/ui/raidInstances/RaidInstanceLoader.tsx
rename to src/ui/raidInstance/RaidInstanceLoader.tsx
diff --git a/src/ui/raidInstances/RaidInstanceTab.tsx b/src/ui/raidInstance/RaidInstanceTab.tsx
similarity index 100%
rename from src/ui/raidInstances/RaidInstanceTab.tsx
rename to src/ui/raidInstance/RaidInstanceTab.tsx
diff --git a/src/ui/raidInstance/creator/RaidInstanceCreator.tsx b/src/ui/raidInstance/creator/RaidInstanceCreator.tsx
new file mode 100644
index 0000000..0bad77a
--- /dev/null
+++ b/src/ui/raidInstance/creator/RaidInstanceCreator.tsx
@@ -0,0 +1,129 @@
+import { ClassGroup } from "@/interface/ClassGroup";
+import { GridLocation } from "@/interface/GridLocation";
+import { PersonCharacter } from "@/interface/PersonCharacter";
+import { RaidInstance } from "@/interface/RaidInstance";
+import { RaidInstancePersonCharacterXref } from "@/interface/RaidInstancePersonCharacterXref";
+import { useRaidInstanceContext } from "@/providers/RaidInstanceLayoutProvider";
+import SelectClassGroupModal from "@/ui/classGroup/modal/SelectClassGroupModal";
+import PersonCharacterSelectorModal from "@/ui/personCharacter/modal/PersonCharacterSelectorModal";
+import { useState } from "react";
+import RaidInstanceCreatorTable from "./RaidInstanceCreatorTable";
+
+
+export default function RaidInstanceCreator(){
+ const {
+ raidGroup,
+ classGroups,
+ raidInstance, setRaidInstance,
+ roster,
+ setRaidLayout,
+ selectedClassGroups, setSelectedClassGroups,
+ personCharacters,
+ personCharacterXrefs, setPersonCharacterXrefs
+ } = useRaidInstanceContext();
+ const [ displayClassGroupsSelectorModal, setDisplayClassGroupsSelectorModal ] = useState(false);
+ const [ displayPersonCharacterSelectorModal, setDisplayPersonCharacterSelectorModal ] = useState(false);
+ const [ currentLocation, setCurrentLocation ] = useState({row: 0, col: 0});
+
+
+ const updateClassGroupsLayout = (newSelectedClassGroup: ClassGroup | null | undefined) => {
+ //Get all existing xrefs, leaving out the current xref it it's been removed
+ const newClassGroups = selectedClassGroups;
+ newClassGroups[currentLocation.col] = newSelectedClassGroup ?? null;
+
+ //Update the raid layout to persist the changes
+ setSelectedClassGroups(newClassGroups);
+
+ setRaidInstance({...raidInstance, raidLayoutId: undefined} as RaidInstance);
+ setRaidLayout(undefined);
+
+ //Hide modal
+ setDisplayClassGroupsSelectorModal(false);
+ }
+
+ const setCharacterIdInCurrentSlot = (newPersonCharacterId: string | undefined) => {
+ let newPersonCharacterXrefs: RaidInstancePersonCharacterXref[] = personCharacterXrefs;
+ //Step through this to make sure it's working as expected
+ if(newPersonCharacterId && (newPersonCharacterId !== "")){
+ const existingXref = personCharacterXrefs?.find((xref) => xref.groupNumber === currentLocation.row && xref.positionNumber == currentLocation.col);
+ //If the ID exists and the xref also exists then update the xref
+ if(existingXref){
+ newPersonCharacterXrefs = personCharacterXrefs?.map((xref) => {
+ if((xref.groupNumber === currentLocation.row) && (xref.positionNumber === currentLocation.col)){
+ xref.personCharacterId = newPersonCharacterId;
+ }
+ return xref;
+ });
+ }
+ //If the ID exists and the xref doesn't add a new xref
+ else{
+ newPersonCharacterXrefs.push(
+ {
+ raidInstanceId: raidInstance?.raidInstanceId ?? "",
+ personCharacterId: newPersonCharacterId,
+ groupNumber: currentLocation.row,
+ positionNumber: currentLocation.col
+ }
+ );
+ }
+ }
+ else{
+ //If the ID doesn't exist then remove the xref if it exists
+ newPersonCharacterXrefs = personCharacterXrefs?.filter((xref) => !(xref.groupNumber === currentLocation.row && xref.positionNumber == currentLocation.col));
+ }
+ setPersonCharacterXrefs(newPersonCharacterXrefs);
+ }
+
+ const getCurrentRunCharacters = (): PersonCharacter[] => {
+ const characterIds = personCharacterXrefs?.filter((xref) => xref.groupNumber === currentLocation.row).map((xref) => xref.personCharacterId);
+ return personCharacters.filter((personCharacter) => characterIds.includes(personCharacter.personCharacterId ?? ""));
+ }
+
+ const getCharactersFromOtherRuns = (): PersonCharacter[] => {
+ const characterIds = personCharacterXrefs?.filter((xref) => xref.groupNumber !== currentLocation.row).map((xref) => xref.personCharacterId);
+ return personCharacters.filter((personCharacter) => characterIds.includes(personCharacter.personCharacterId ?? ""));
+ }
+
+ const getCharacterFromCell = () => {
+ const xref = personCharacterXrefs.find((xref) => xref.groupNumber === currentLocation.row && xref.positionNumber === currentLocation.col);
+ return personCharacters.find((personCharacter) => personCharacter.personCharacterId === xref?.personCharacterId);
+ }
+
+ const getPersonCharactersFromRoster = (): PersonCharacter[] => {
+ const personIds = roster.map((person) => person.personId);
+ return personCharacters.filter((personCharacter) => personIds.includes(personCharacter.personId));
+ }
+
+
+
+
+ return (
+
+ {/* Main Content */}
+
{ setCurrentLocation({row: 0, col: col}); setDisplayClassGroupsSelectorModal(true); }}
+ onClickBodyCell={(row, col) => { setCurrentLocation({row: row, col: col}); setDisplayPersonCharacterSelectorModal(true); }}
+ />
+ {/* Modals */}
+ setDisplayClassGroupsSelectorModal(false)}
+ onChange={updateClassGroupsLayout}
+ selectedClassGroup={selectedClassGroups[currentLocation.col]}
+ raidGroupId={raidGroup?.raidGroupId ?? ""}
+ />
+ setDisplayPersonCharacterSelectorModal(false)}
+ currentSlotClassGroup={classGroups[currentLocation.col]}
+ currentRunCharacters={getCurrentRunCharacters()}
+ otherRunsCharacters={getCharactersFromOtherRuns()}
+ personCharacters={getPersonCharactersFromRoster()}
+ selectedCharacterId={getCharacterFromCell()?.personCharacterId}
+ setInput={setCharacterIdInCurrentSlot}
+ />
+
+ );
+}
diff --git a/src/ui/raidInstance/creator/RaidInstanceCreatorTable.tsx b/src/ui/raidInstance/creator/RaidInstanceCreatorTable.tsx
new file mode 100644
index 0000000..340c279
--- /dev/null
+++ b/src/ui/raidInstance/creator/RaidInstanceCreatorTable.tsx
@@ -0,0 +1,51 @@
+import PrimaryButton from "@/components/button/PrimaryButton";
+import { RaidInstance } from "@/interface/RaidInstance";
+import { useRaidInstanceContext } from "@/providers/RaidInstanceLayoutProvider";
+import RaidInstanceCreatorTableBody from "./RaidInstanceCreatorTableBody";
+import RaidInstanceCreatorTableHeader from "./RaidInstanceCreatorTableHeader";
+
+
+export default function RaidInstanceCreatorTable({
+ onClickHeaderCell,
+ onClickBodyCell
+}:{
+ onClickHeaderCell: (col: number) => void;
+ onClickBodyCell: (row: number, col: number) => void;
+}){
+ const { raidInstance, setRaidInstance } = useRaidInstanceContext();
+
+
+ const addRun = () => {
+ const newRaidInstance = {...raidInstance};
+ newRaidInstance.numberRuns = (newRaidInstance.numberRuns ?? 0) + 1;
+ setRaidInstance(newRaidInstance as RaidInstance);
+ }
+
+
+ return(
+
+
+
+ {/* Header */}
+
+ {/* Body */}
+
+
+
+ {/* Buttons */}
+
+ Add Run
+
+
+ );
+}
diff --git a/src/ui/raidInstance/creator/RaidInstanceCreatorTableBody.tsx b/src/ui/raidInstance/creator/RaidInstanceCreatorTableBody.tsx
new file mode 100644
index 0000000..2544493
--- /dev/null
+++ b/src/ui/raidInstance/creator/RaidInstanceCreatorTableBody.tsx
@@ -0,0 +1,132 @@
+import DangerButton from "@/components/button/DangerButton";
+import TertiaryButton from "@/components/button/TertiaryButton";
+import { PersonCharacter } from "@/interface/PersonCharacter";
+import { RaidInstance } from "@/interface/RaidInstance";
+import { useRaidInstanceContext } from "@/providers/RaidInstanceLayoutProvider";
+import { getPersonCharactersFromXrefs } from "@/util/PersonCharacterUtil";
+import moment from "moment";
+import { BsDiscord, BsXLg } from "react-icons/bs";
+
+
+export default function RaidInstanceCreatorTableBody({
+ onClickBodyCell
+}:{
+ onClickBodyCell: (run: number, slot: number) => void;
+}){
+ const {
+ raidInstance, setRaidInstance,
+ people,
+ personCharacterXrefs, setPersonCharacterXrefs,
+ personCharacters,
+ selectedClassGroups
+ } = useRaidInstanceContext();
+
+
+ const characterGrid: (PersonCharacter | null)[][] = getPersonCharactersFromXrefs(personCharacterXrefs, personCharacters, raidInstance);
+
+
+ const removeRun = (runNumber: number) => {
+ const newXrefs = personCharacterXrefs.filter((xref) => xref.groupNumber !== runNumber)?.map((xref) => {return {...xref, groupNumber: xref.groupNumber >= runNumber ? xref.groupNumber - 1 : xref.groupNumber}});
+ setPersonCharacterXrefs(newXrefs);
+ setRaidInstance({...raidInstance, numberRuns: (raidInstance?.numberRuns ?? 1) - 1} as RaidInstance);
+ }
+
+ const copyDiscordStringToClipBoard = (run: number) => {
+ let discordString = "";
+ //Instance name
+ discordString += `${raidInstance?.raidInstanceName}\n`;
+
+ //Start time
+ discordString += moment(raidInstance?.raidStartDate).format("MM/DD/YYYY HH:mm") + " - ";
+ //End time
+ discordString += moment(raidInstance?.raidEndDate).format("MM/DD/YYYY HH:mm") + "\n";
+
+ //Characters
+ characterGrid[run].forEach((ch, index) => {
+ const person = people.find((p) => p.personId === ch?.personId);
+ if(person){
+ //Discord ID / name
+ discordString += (person.discordId && (person.discordId !== "")) ? "@" + person.discordId : person.personName;
+ discordString += ": ";
+ //Character Name
+ discordString += ch?.characterName;
+ }
+ else{
+ const classGroup = selectedClassGroups[index];
+ //Class Group
+ discordString += classGroup?.classGroupName ?? "Any Class";
+ discordString += ": ";
+ //Any
+ discordString += "None";
+ }
+ discordString += "\n";
+ });
+
+ navigator.clipboard.writeText(discordString);
+ }
+
+
+ return (
+
+ {
+ characterGrid.map((run, runIndex) => (
+
+ {
+ run.map((ch, chIndex) => (
+ onClickBodyCell(runIndex, chIndex)}
+ >
+
+ {
+ ch?.gameClassId &&
+ 
+ }
+ {ch ? ch.characterName : "None"}
+
+ |
+ ))
+ }
+
+
+ removeRun(runIndex)}
+ >
+
+
+ copyDiscordStringToClipBoard(runIndex)}
+ >
+
+
+
+ |
+
+ ))
+ }
+
+ );
+}
\ No newline at end of file
diff --git a/src/ui/raidInstance/creator/RaidInstanceCreatorTableHeader.tsx b/src/ui/raidInstance/creator/RaidInstanceCreatorTableHeader.tsx
new file mode 100644
index 0000000..96f8147
--- /dev/null
+++ b/src/ui/raidInstance/creator/RaidInstanceCreatorTableHeader.tsx
@@ -0,0 +1,36 @@
+import { useRaidInstanceContext } from "@/providers/RaidInstanceLayoutProvider";
+
+
+export default function RaidInstanceCreatorTableHeader({
+ onClickHeaderCell
+}:{
+ onClickHeaderCell: (slot: number) => void;
+}){
+ const { selectedClassGroups } = useRaidInstanceContext();
+
+
+ return (
+
+
+ {
+ selectedClassGroups.map((classGroup, index) => (
+ | onClickHeaderCell(index)}
+ >
+ {classGroup?.classGroupName ?? "Any"}
+ |
+ ))
+ }
+
+
+ |
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/ui/raidInstance/creator/RaidInstanceCreatorUI.tsx b/src/ui/raidInstance/creator/RaidInstanceCreatorUI.tsx
new file mode 100644
index 0000000..91377d8
--- /dev/null
+++ b/src/ui/raidInstance/creator/RaidInstanceCreatorUI.tsx
@@ -0,0 +1,12 @@
+import RaidInstanceHeader from "../RaidInstanceHeader";
+import RaidInstanceCreator from "./RaidInstanceCreator";
+
+
+export default function RaidInstanceCreatorUI(){
+ return (
+ <>
+
+
+ >
+ );
+}
diff --git a/src/ui/raidInstances/modals/DeleteRaidInstanceModal.tsx b/src/ui/raidInstance/modals/DeleteRaidInstanceModal.tsx
similarity index 100%
rename from src/ui/raidInstances/modals/DeleteRaidInstanceModal.tsx
rename to src/ui/raidInstance/modals/DeleteRaidInstanceModal.tsx
diff --git a/src/ui/raidInstances/modals/RaidInstanceModal.tsx b/src/ui/raidInstance/modals/RaidInstanceModal.tsx
similarity index 96%
rename from src/ui/raidInstances/modals/RaidInstanceModal.tsx
rename to src/ui/raidInstance/modals/RaidInstanceModal.tsx
index 0d660f6..c4bb618 100644
--- a/src/ui/raidInstances/modals/RaidInstanceModal.tsx
+++ b/src/ui/raidInstance/modals/RaidInstanceModal.tsx
@@ -25,8 +25,8 @@ export default function RaidInstanceModal({
const [raidInstanceName, setRaidInstanceName] = useState("");
const [raidStartDate, setRaidStartDate] = useState(new Date());
const [raidEndDate, setRaidEndDate] = useState(new Date());
- const [raidSize, setRaidSize] = useState(0);
- const [numberRuns, setNumberRuns] = useState(0);
+ const [raidSize, setRaidSize] = useState(3);
+ const [numberRuns, setNumberRuns] = useState(1);
const modalId = crypto.randomUUID().replaceAll("-", "");
const createRaidInstanceMutate = useCreateRaidInstance(raidGroup.raidGroupId ?? "");
@@ -48,10 +48,10 @@ export default function RaidInstanceModal({
setRaidInstanceName("");
setRaidStartDate(currentDate);
setRaidEndDate(futureDate);
- setRaidSize(0);
- setNumberRuns(0);
+ setRaidSize(3);
+ setNumberRuns(1);
}
- }, [ raidInstance ]);
+ }, [ display, raidInstance ]);
useEffect(() => {
if(createRaidInstanceMutate.status === "success"){
diff --git a/src/ui/raidLayout/modal/RaidLayoutSelectorModal.tsx b/src/ui/raidLayout/modal/RaidLayoutSelectorModal.tsx
new file mode 100644
index 0000000..773fcee
--- /dev/null
+++ b/src/ui/raidLayout/modal/RaidLayoutSelectorModal.tsx
@@ -0,0 +1,135 @@
+import PrimaryButton from "@/components/button/PrimaryButton";
+import SecondaryButton from "@/components/button/SecondaryButton";
+import TextInput from "@/components/input/TextInput";
+import RaidBuilderModal from "@/components/modal/RaidBuilderModal";
+import Pagination from "@/components/pagination/Pagination";
+import { RaidLayout } from "@/interface/RaidLayout";
+import { useEffect, useState } from "react";
+
+
+export default function RaidLayoutSelectorModal({
+ display,
+ close,
+ onSubmit,
+ raidLayouts,
+ selectedRaidLayoutId
+}:{
+ display: boolean;
+ close: () => void;
+ onSubmit: (raidLayoutId?: string) => void;
+ raidLayouts: RaidLayout[];
+ selectedRaidLayoutId?: string;
+}){
+ const pageSize = 16;
+ const modalId = crypto.randomUUID().replaceAll("-", "");
+ const [ page, setPage ] = useState(1);
+ const [ searchTerm, setSearchTerm ] = useState("");
+ const [ matchingLayouts, setMatchingLayouts ] = useState(raidLayouts);
+ const [ currentlyVisibleLayouts, setCurrentlyVisibleLayouts ] = useState(raidLayouts);
+ const [ currentlySelectedLayoutId, setCurrentlySelectedLayoutId ] = useState(selectedRaidLayoutId?.slice((page - 1) * pageSize, page * pageSize));
+
+
+ const updateInput = (raidLayoutId: string | undefined) => {
+ if(raidLayoutId === currentlySelectedLayoutId){
+ setCurrentlySelectedLayoutId(undefined);
+ }
+ else{
+ setCurrentlySelectedLayoutId(raidLayoutId);
+ }
+ }
+
+ //Update any changes to Raid Layout ID
+ useEffect(() => {
+ setCurrentlySelectedLayoutId(selectedRaidLayoutId);
+ }, [ raidLayouts, selectedRaidLayoutId ]);
+
+ //Update page data when modal becomes visible
+ useEffect(() => {
+ setPage(1);
+ setSearchTerm("");
+ }, [ display ]);
+
+ //Update visible people when paging info is updated
+ useEffect(() => {
+ const filteredLayouts = raidLayouts.filter((raidLayout) => raidLayout.raidLayoutName.toLowerCase().includes(searchTerm?.toLowerCase() ?? ""));
+
+ setMatchingLayouts(filteredLayouts);
+ setCurrentlyVisibleLayouts(filteredLayouts.slice((page - 1) * pageSize, page * pageSize));
+ }, [ page, searchTerm, raidLayouts ]);
+
+
+ return (
+
+ {/* Search Box */}
+
+ setSearchTerm(e.target.value)}
+ placeholder="Search"
+ />
+
+ {/* Raid Layouts */}
+
+ {
+ currentlyVisibleLayouts.map((raidLayout) => (
+
+ {}}
+ onClick={() => updateInput(raidLayout.raidLayoutId)}
+ />
+
+ {raidLayout.raidLayoutName}
+
+
+ ))
+ }
+
+ {/* Pagination */}
+
+
+ }
+ modalFooter={
+ <>
+ {onSubmit(currentlySelectedLayoutId); close();}}
+ >
+ Select
+
+
+ Cancel
+
+ >
+ }
+ />
+ );
+}
diff --git a/src/util/PersonCharacterUtil.ts b/src/util/PersonCharacterUtil.ts
new file mode 100644
index 0000000..11bff4a
--- /dev/null
+++ b/src/util/PersonCharacterUtil.ts
@@ -0,0 +1,63 @@
+import { GameClass } from "@/interface/GameClass";
+import { PersonCharacter } from "@/interface/PersonCharacter";
+import { RaidInstance } from "@/interface/RaidInstance";
+import { RaidInstancePersonCharacterXref } from "@/interface/RaidInstancePersonCharacterXref";
+
+
+export function getCharactersThatMatchClassGroup(gameCharacters: PersonCharacter[], gameClasses: GameClass[]): PersonCharacter[]{
+ const matchingCharacters: PersonCharacter[] = [];
+
+ for(const ch of gameCharacters){
+ if(gameClasses.length === 0){
+ matchingCharacters.push(ch);
+ continue;
+ }
+ for(const cl of gameClasses ?? []){
+ if((cl.gameClassId === ch.gameClassId)){
+ matchingCharacters.push(ch);
+ break;
+ }
+ }
+ }
+
+ return matchingCharacters;
+}
+
+export function getCharactersThatNotMatchClassGroup(gameCharacters: PersonCharacter[], gameClasses: GameClass[]): PersonCharacter[]{
+ const nonMatchingCharacters: PersonCharacter[] = [];
+
+ for(const ch of gameCharacters){
+ let foundMatch = false;
+ if(gameClasses.length === 0){
+ foundMatch = true;
+ continue;
+ }
+ for(const cl of gameClasses ?? []){
+ if(cl.gameClassId === ch.gameClassId){
+ foundMatch = true;
+ break;
+ }
+ }
+ if(!foundMatch){
+ nonMatchingCharacters.push(ch);
+ }
+ }
+
+ return nonMatchingCharacters;
+}
+
+export function getPersonCharactersFromXrefs(raidInstancePersonCharacterXrefs: RaidInstancePersonCharacterXref[], personCharacters: (PersonCharacter | null)[], raidInstance?: RaidInstance){
+ const personCharacterGrid: (PersonCharacter | null)[][] = [];
+ for(let row = 0;row < (raidInstance?.numberRuns ?? 0);++row){
+ personCharacterGrid.push([]);
+ for(let col = 0;col < (raidInstance?.raidSize ?? 0);++col){
+ personCharacterGrid[row].push(null);
+ }
+ }
+
+ raidInstancePersonCharacterXrefs.forEach((xref) => {
+ personCharacterGrid[xref.groupNumber][xref.positionNumber] = personCharacters.find((pc) => pc?.personCharacterId === xref.personCharacterId) ?? null;
+ });
+
+ return personCharacterGrid;
+}