From 56236fd2ace4d450508757463dd4352927c4efac Mon Sep 17 00:00:00 2001 From: Mattrixwv Date: Sat, 15 Mar 2025 12:20:05 -0400 Subject: [PATCH] Raid Instance Creator working --- src/components/input/TextInput.tsx | 6 +- .../PersonCharacterSelector.tsx | 76 +++++++ .../personCharacter/RatingSelector.tsx | 15 ++ src/hooks/ClassGroupHooks.ts | 5 +- src/hooks/GameClassHooks.ts | 2 +- src/hooks/PersonCharacterHooks.ts | 19 ++ src/hooks/RaidInstanceHooks.ts | 62 +++++- src/hooks/RaidLayoutHooks.ts | 20 +- src/interface/GridLocation.ts | 4 + .../RaidInstancePersonCharacterXref.ts | 7 + src/pages/protected/RaidGroupPage.tsx | 2 +- src/pages/protected/RaidInstancePage.tsx | 38 +++- src/providers/RaidInstanceLayoutProvider.tsx | 193 ++++++++++++++++ src/providers/TimedModalProvider.tsx | 2 +- .../modal/SelectClassGroupModal.tsx | 4 +- src/ui/person/modals/PersonSelectorModal.tsx | 137 ++++++++++++ .../modal/PersonCharacterSelectorModal.tsx | 177 +++++++++++++++ .../RaidInstanceAdminButtons.tsx | 0 src/ui/raidInstance/RaidInstanceHeader.tsx | 207 ++++++++++++++++++ .../RaidInstanceList.tsx | 0 .../RaidInstanceListSkeleton.tsx | 0 .../RaidInstanceLoader.tsx | 0 .../RaidInstanceTab.tsx | 0 .../creator/RaidInstanceCreator.tsx | 129 +++++++++++ .../creator/RaidInstanceCreatorTable.tsx | 51 +++++ .../creator/RaidInstanceCreatorTableBody.tsx | 132 +++++++++++ .../RaidInstanceCreatorTableHeader.tsx | 36 +++ .../creator/RaidInstanceCreatorUI.tsx | 12 + .../modals/DeleteRaidInstanceModal.tsx | 0 .../modals/RaidInstanceModal.tsx | 10 +- .../modal/RaidLayoutSelectorModal.tsx | 135 ++++++++++++ src/util/PersonCharacterUtil.ts | 63 ++++++ 32 files changed, 1524 insertions(+), 20 deletions(-) create mode 100644 src/components/personCharacter/PersonCharacterSelector.tsx create mode 100644 src/interface/GridLocation.ts create mode 100644 src/interface/RaidInstancePersonCharacterXref.ts create mode 100644 src/providers/RaidInstanceLayoutProvider.tsx create mode 100644 src/ui/person/modals/PersonSelectorModal.tsx create mode 100644 src/ui/personCharacter/modal/PersonCharacterSelectorModal.tsx rename src/ui/{raidInstances => raidInstance}/RaidInstanceAdminButtons.tsx (100%) create mode 100644 src/ui/raidInstance/RaidInstanceHeader.tsx rename src/ui/{raidInstances => raidInstance}/RaidInstanceList.tsx (100%) rename src/ui/{raidInstances => raidInstance}/RaidInstanceListSkeleton.tsx (100%) rename src/ui/{raidInstances => raidInstance}/RaidInstanceLoader.tsx (100%) rename src/ui/{raidInstances => raidInstance}/RaidInstanceTab.tsx (100%) create mode 100644 src/ui/raidInstance/creator/RaidInstanceCreator.tsx create mode 100644 src/ui/raidInstance/creator/RaidInstanceCreatorTable.tsx create mode 100644 src/ui/raidInstance/creator/RaidInstanceCreatorTableBody.tsx create mode 100644 src/ui/raidInstance/creator/RaidInstanceCreatorTableHeader.tsx create mode 100644 src/ui/raidInstance/creator/RaidInstanceCreatorUI.tsx rename src/ui/{raidInstances => raidInstance}/modals/DeleteRaidInstanceModal.tsx (100%) rename src/ui/{raidInstances => raidInstance}/modals/RaidInstanceModal.tsx (96%) create mode 100644 src/ui/raidLayout/modal/RaidLayoutSelectorModal.tsx create mode 100644 src/util/PersonCharacterUtil.ts 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 */} +
+ { + + Save + + } +
+
+
+ ); +} 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)} + /> + +
+ )) + } +
+ {/* 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; +}