Raid Instance Creator working

This commit is contained in:
2025-03-15 12:20:05 -04:00
parent cff5e098b8
commit 56236fd2ac
32 changed files with 1524 additions and 20 deletions

View File

@@ -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 RaidInstanceAdminButtons({
buttonProps,
showRaidInstanceModal,
showDeleteRaidInstanceModal
}:{
buttonProps: ButtonProps;
showRaidInstanceModal: () => void;
showDeleteRaidInstanceModal: () => void;
}){
return (
<div
className="flex flex-row gap-2"
>
<PrimaryButton
{...buttonProps}
onClick={showRaidInstanceModal}
>
<BsPencilFill
size={22}
/>
</PrimaryButton>
<DangerButton
{...buttonProps}
onClick={showDeleteRaidInstanceModal}
>
<BsTrash3
size={22}
/>
</DangerButton>
</div>
);
}

View File

@@ -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 (
<div
className="flex flex-col items-center justify-center w-full h-full mb-16"
>
{/* Raid Instance Name */}
<div
className="flex flex-col justify-center items-center min-w-[60rem] text-center"
>
<h1
className="flex flex-row items-center justify-center text-4xl"
>
<TextInput
id={`raidInstance${raidInstance?.raidInstanceId}Name`}
inputClasses="text-center"
placeholder="Raid Instance Name"
value={raidInstance?.raidInstanceName ?? ""}
onChange={(e) =>
setRaidInstance({
...raidInstance,
raidInstanceName: e.target.value
} as RaidInstance)
}
//disabled={}
/>
</h1>
</div>
{/* Start and End Dates */}
<div
className="flex flex-row justify-center mt-4"
>
<DateInput
id={`raidInstance${raidInstance?.raidInstanceId}StartDate`}
placeholder="Start Date"
value={moment(raidInstance?.raidStartDate ?? new Date()).format("YYYY-MM-DDTHH:mm")}
onChange={(e) => setRaidInstance({...raidInstance, raidStartDate: moment(e.target.value).toDate()} as RaidInstance)}
/>
<DateInput
id={`raidInstance${raidInstance?.raidInstanceId}EndDate`}
placeholder="End Date"
value={moment(raidInstance?.raidEndDate).format("YYYY-MM-DDTHH:mm")}
onChange={(e) => setRaidInstance({...raidInstance, raidEndDate: moment(e.target.value).toDate()} as RaidInstance)}
/>
</div>
{/* Raid Size & Layout */}
<div
className="flex flex-row items-center justify-center gap-x-4 mt-4"
>
{/* Raid Size */}
<div
className="flex flex-row items-center justify-center"
>
<NumberInput
id={`raidInstance${raidInstance?.raidInstanceId}Size`}
label="Raid Size"
value={raidInstance?.raidSize ?? 0}
onChange={updateRaidSize}
min={0}
max={100}
/>
</div>
{/* Raid Layout */}
<div
className="flex flex-row items-center justify-center"
>
<div
className="bg-inherit px-4 rounded-sm w-auto"
onClick={() => setDisplayRaidLayoutSelectorModal(true)}
>
<div
className="relative bg-inherit"
>
<div
id="selectRaidLayoutButton"
className="px-4 py-2 rounded-lg whitespace-nowrap cursor-pointer ring-2 ring-gray-500"
>
{raidLayout?.raidLayoutId ? raidLayout.raidLayoutName : "Select Raid Layout"}
</div>
<div
className={clsx(
"absolute cursor-text left-0 -top-3 mx-1 px-1 whitespace-nowrap",
"bg-white dark:bg-neutral-800 text-sm text-gray-500"
)}
style={{transitionProperty: "top, font-size, line-height", transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)", transitionDuration: "150ms"}}
>
Raid Layout
</div>
</div>
</div>
<RaidLayoutSelectorModal
raidLayouts={raidLayouts}
selectedRaidLayoutId={raidLayout?.raidLayoutId}
display={displayRaidLayoutSelectorModal}
close={() => setDisplayRaidLayoutSelectorModal(false)}
onSubmit={updateRaidLayout}
/>
</div>
{/* Roster */}
<div
className="flex flex-row items-center justify-center"
>
<PrimaryButton
onClick={() => setDisplayRosterSelectorModal(true)}
>
Roster
</PrimaryButton>
<PersonSelectorModal
display={displayRosterSelectorModal}
close={() => setDisplayRosterSelectorModal(false)}
onSubmit={(newRosterIds) => setRoster(people.filter(person => newRosterIds.includes(person.personId ?? "")))}
people={people}
selectedPersonIds={roster.map(person => person.personId ?? "")}
/>
</div>
{/* Save Button */}
<div
className="flex flex-row items-center justify-center ml-4"
>
{
<PrimaryButton
onClick={saveRaidInstance}
>
Save
</PrimaryButton>
}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,124 @@
import { ButtonProps } from "@/components/button/Button";
import Table from "@/components/table/Table";
import { RaidGroup } from "@/interface/RaidGroup";
import { RaidInstance } from "@/interface/RaidInstance";
import moment from "moment";
import { useState } from "react";
import { Link } from "react-router";
import DeleteRaidInstanceModal from "./modals/DeleteRaidInstanceModal";
import RaidInstanceModal from "./modals/RaidInstanceModal";
import RaidInstanceAdminButtons from "./RaidInstanceAdminButtons";
export default function RaidInstanceList({
raidInstances,
raidGroup
}:{
raidInstances: RaidInstance[];
raidGroup: RaidGroup;
}){
const [ selectedRaidInstance, setSelectedRaidInstance ] = useState<RaidInstance>();
const [ displayEditRaidInstanceModal, setDisplayEditRaidInstanceModal ] = useState(false);
const [ displayDeleteRaidInstanceModal, setDisplayDeleteRaidInstanceModal ] = useState(false);
const buttonProps: ButtonProps = {
variant: "ghost",
size: "md",
shape: "square"
};
const headElements: React.ReactNode[] = [
<div
className="text-nowrap"
>
Raid Instance
</div>,
<div
className="text-nowrap"
>
Start Date
</div>,
<div
className="text-nowrap"
>
End Date
</div>,
<div
className="text-nowrap"
>
Size
</div>,
<div
className="text-nowrap"
>
Runs
</div>,
<div
className="text-nowrap pl-16"
>
Actions
</div>
];
const bodyElements: React.ReactNode[][] = raidInstances.map((raidInstance) => [
<Link
to={`/raidGroup/${raidInstance.raidGroupId}/raidInstance/${raidInstance.raidInstanceId}`}
className="text-nowrap"
>
{raidInstance.raidInstanceName}
</Link>,
<div
className="text-nowrap"
>
{moment(raidInstance.raidStartDate).format("MM-DD-YYYY HH:mm")}
</div>,
<div
className="text-nowrap"
>
{moment(raidInstance.raidEndDate).format("MM-DD-YYYY HH:mm")}
</div>,
<div>
{raidInstance.raidSize}
</div>,
<div>
{raidInstance.numberRuns}
</div>,
<div
className="flex flex-row items-center justify-center gap-2 pl-16"
>
<div
className="py-4 border-l border-neutral-500"
>
&nbsp;
</div>
<RaidInstanceAdminButtons
buttonProps={buttonProps}
showRaidInstanceModal={() => { setSelectedRaidInstance(raidInstance); setDisplayEditRaidInstanceModal(true); }}
showDeleteRaidInstanceModal={() => { setSelectedRaidInstance(raidInstance); setDisplayDeleteRaidInstanceModal(true); }}
/>
</div>
]);
return (
<>
<Table
tableHeadElements={headElements}
tableBodyElements={bodyElements}
/>
<RaidInstanceModal
display={displayEditRaidInstanceModal}
close={() => { setDisplayEditRaidInstanceModal(false); setSelectedRaidInstance(undefined); }}
raidInstance={selectedRaidInstance}
raidGroup={raidGroup}
/>
<DeleteRaidInstanceModal
display={displayDeleteRaidInstanceModal}
close={() => { setDisplayDeleteRaidInstanceModal(false); setSelectedRaidInstance(undefined); }}
raidInstance={selectedRaidInstance}
/>
</>
);
}

View File

@@ -0,0 +1,95 @@
import { ButtonShape, ButtonSizeType, ButtonVariant } from "@/components/button/Button";
import Table from "@/components/table/Table";
import { elementBg } from "@/util/SkeletonUtil";
import RaidInstanceAdminButtons from "./RaidInstanceAdminButtons";
export default function RaidInstanceListSkeleton(){
const headElements: React.ReactNode[] = [
<div
className="text-nowrap"
>
Raid Instance
</div>,
<div
className="text-nowrap"
>
Start Date
</div>,
<div
className="text-nowrap"
>
End Date
</div>,
<div>
Size
</div>,
<div>
Runs
</div>,
<div>
Actions
</div>
];
const bodyElements: React.ReactNode[][] = [
RaidInstanceSkeleton(),
RaidInstanceSkeleton(),
RaidInstanceSkeleton(),
RaidInstanceSkeleton(),
RaidInstanceSkeleton(),
RaidInstanceSkeleton(),
RaidInstanceSkeleton(),
RaidInstanceSkeleton(),
RaidInstanceSkeleton(),
RaidInstanceSkeleton()
];
return (
<Table
tableHeadElements={headElements}
tableBodyElements={bodyElements}
/>
);
}
function RaidInstanceSkeleton(){
const buttonsProps = {
buttonProps: {
variant: "ghost" as ButtonVariant,
size: "md" as ButtonSizeType,
shape: "square" as ButtonShape,
disabled: true
},
showRaidInstanceModal: () => {},
showDeleteRaidInstanceModal: () => {}
};
const elements: React.ReactNode[] = [
<div
className={`h-6 w-32 mr-2 ${elementBg}`}
/>,
<div
className={`h-6 w-[26rem] mr-2 ${elementBg}`}
/>,
<div
className={`h-6 w-[26rem] mr-2 ${elementBg}`}
/>,
<div
className={`h-6 w-32 mr-2 ${elementBg}`}
/>,
<div
className={`h-6 w-32 mr-2 ${elementBg}`}
/>,
<div
className="flex flex-row items-center justify-center gap-2 pl-16"
>
<div className="py-4 border-l border-neutral-500">&nbsp;</div>
<RaidInstanceAdminButtons {...buttonsProps}/>
</div>
];
return elements;
}

View File

@@ -0,0 +1,36 @@
import DangerMessage from "@/components/message/DangerMessage";
import { useGetRaidInstancesByRaidGroup } from "@/hooks/RaidInstanceHooks";
import { RaidGroup } from "@/interface/RaidGroup";
import RaidInstanceList from "./RaidInstanceList";
import RaidInstanceListSkeleton from "./RaidInstanceListSkeleton";
export default function RaidInstanceLoader({
raidGroup,
page,
pageSize,
searchTerm
}:{
raidGroup: RaidGroup;
page: number;
pageSize: number;
searchTerm?: string;
}){
const raidInstancesQuery = useGetRaidInstancesByRaidGroup(raidGroup.raidGroupId ?? "", page - 1, pageSize, searchTerm);
if(raidInstancesQuery.status === "pending"){
return <RaidInstanceListSkeleton/>
}
else if(raidInstancesQuery.status === "error"){
return <DangerMessage>Error: {raidInstancesQuery.error.message}</DangerMessage>
}
else{
return (
<RaidInstanceList
raidInstances={raidInstancesQuery.data ?? []}
raidGroup={raidGroup}
/>
);
}
}

View File

@@ -0,0 +1,105 @@
import PrimaryButton from "@/components/button/PrimaryButton";
import TextInput from "@/components/input/TextInput";
import Pagination from "@/components/pagination/Pagination";
import { useGetRaidInstancesByRaidGroupCount } from "@/hooks/RaidInstanceHooks";
import { RaidGroup } from "@/interface/RaidGroup";
import { useEffect, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import RaidInstanceLoader from "./RaidInstanceLoader";
import RaidInstanceModal from "./modals/RaidInstanceModal";
export default function RaidInstanceTab({
raidGroup
}:{
raidGroup: RaidGroup;
}){
const [ displayCreateRaidInstanceModal, setDisplayCreateRaidInstanceModal ] = useState(false);
const [ page, setPage ] = useState(1);
const [ totalPages, setTotalPages ] = useState(1);
const [ searchTerm, setSearchTerm ] = useState("");
const [ sentSearchTerm, setSentSearchTerm ] = useState<string>();
const pageSize = 10;
const modalId = crypto.randomUUID().replaceAll("-", "");
const updateSearchTerm = useDebouncedCallback((newSearchTerm: string) => {
setSentSearchTerm(newSearchTerm);
}, 1000);
useEffect(() => {
updateSearchTerm(searchTerm);
}, [ searchTerm, updateSearchTerm ]);
const raidInstanceCountQuery = useGetRaidInstancesByRaidGroupCount(raidGroup.raidGroupId!, sentSearchTerm);
useEffect(() => {
if(raidInstanceCountQuery.status === "success"){
setTotalPages(Math.ceil(raidInstanceCountQuery.data / pageSize));
}
}, [ raidInstanceCountQuery ]);
return (
<>
<div
className="flex flex-row items-center justify-between w-full mb-8"
>
<div
className="flex flex-row items-center justify-start w-full"
>
&nbsp;
</div>
{/* Add Raid Instance Button */}
<div
className="flex flex-row items-center justify-center w-full"
>
<PrimaryButton
className="text-nowrap"
onClick={() => setDisplayCreateRaidInstanceModal(true)}
>
Create Raid Instance
</PrimaryButton>
<RaidInstanceModal
display={displayCreateRaidInstanceModal}
close={() => setDisplayCreateRaidInstanceModal(false)}
raidInstance={undefined}
raidGroup={raidGroup}
/>
</div>
{/* Raid Instance Search Box */}
<div
className="flex flex-row items-center justify-end w-full"
>
<div>
<TextInput
id={`raidInstanceSearchBox${modalId}`}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search"
/>
</div>
</div>
</div>
{/* Raid Layout List */}
<RaidInstanceLoader
page={page}
pageSize={pageSize}
searchTerm={sentSearchTerm}
raidGroup={raidGroup}
/>
{/* Pagination */}
<div
className="my-12"
>
<Pagination
currentPage={page}
totalPages={totalPages}
onChange={setPage}
/>
</div>
</>
);
}

View File

@@ -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<GridLocation>({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 (
<div
className="flex flex-col items-center justify-center"
>
{/* Main Content */}
<RaidInstanceCreatorTable
onClickHeaderCell={(col) => { setCurrentLocation({row: 0, col: col}); setDisplayClassGroupsSelectorModal(true); }}
onClickBodyCell={(row, col) => { setCurrentLocation({row: row, col: col}); setDisplayPersonCharacterSelectorModal(true); }}
/>
{/* Modals */}
<SelectClassGroupModal
display={displayClassGroupsSelectorModal}
close={() => setDisplayClassGroupsSelectorModal(false)}
onChange={updateClassGroupsLayout}
selectedClassGroup={selectedClassGroups[currentLocation.col]}
raidGroupId={raidGroup?.raidGroupId ?? ""}
/>
<PersonCharacterSelectorModal
display={displayPersonCharacterSelectorModal}
close={() => setDisplayPersonCharacterSelectorModal(false)}
currentSlotClassGroup={classGroups[currentLocation.col]}
currentRunCharacters={getCurrentRunCharacters()}
otherRunsCharacters={getCharactersFromOtherRuns()}
personCharacters={getPersonCharactersFromRoster()}
selectedCharacterId={getCharacterFromCell()?.personCharacterId}
setInput={setCharacterIdInCurrentSlot}
/>
</div>
);
}

View File

@@ -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(
<div
className="flex flex-col items-center justify-center"
>
<div
className="flex flex-col items-start justify-start max-w-full overflow-auto pb-16"
>
<table>
{/* Header */}
<RaidInstanceCreatorTableHeader
onClickHeaderCell={onClickHeaderCell}
/>
{/* Body */}
<RaidInstanceCreatorTableBody
onClickBodyCell={onClickBodyCell}
/>
</table>
</div>
{/* Buttons */}
<PrimaryButton
onClick={addRun}
>
Add Run
</PrimaryButton>
</div>
);
}

View File

@@ -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 (
<tbody
id="instanceTableBody"
>
{
characterGrid.map((run, runIndex) => (
<tr
key={runIndex}
>
{
run.map((ch, chIndex) => (
<td
key={chIndex}
className="px-4 py-2 border-2 cursor-default"
onClick={() => onClickBodyCell(runIndex, chIndex)}
>
<div
className="flex flex-row justify-start flex-nowrap"
>
{
ch?.gameClassId &&
<img
className="mr-2 max-h-8 max-w-8"
src={`${import.meta.env.VITE_ICON_URL}/gameClass/id/${ch.gameClassId}`}
/>
}
{ch ? ch.characterName : "None"}
</div>
</td>
))
}
<td
className="pl-2"
>
<div
className="flex flex-row justify-center items-center cursor-pointer p-2 space-x-2"
>
<DangerButton
variant="ghost"
shape="square"
onClick={() => removeRun(runIndex)}
>
<BsXLg
size={22}
strokeWidth={2}
/>
</DangerButton>
<TertiaryButton
variant="ghost"
shape="square"
onClick={() => copyDiscordStringToClipBoard(runIndex)}
>
<BsDiscord
size={22}
/>
</TertiaryButton>
</div>
</td>
</tr>
))
}
</tbody>
);
}

View File

@@ -0,0 +1,36 @@
import { useRaidInstanceContext } from "@/providers/RaidInstanceLayoutProvider";
export default function RaidInstanceCreatorTableHeader({
onClickHeaderCell
}:{
onClickHeaderCell: (slot: number) => void;
}){
const { selectedClassGroups } = useRaidInstanceContext();
return (
<thead>
<tr
id="instanceTableHead"
>
{
selectedClassGroups.map((classGroup, index) => (
<th
key={index}
className="px-4 py-2 border-2 cursor-pointer"
onClick={() => onClickHeaderCell(index)}
>
{classGroup?.classGroupName ?? "Any"}
</th>
))
}
<th
className="px-4 py-2"
>
&nbsp;
</th>
</tr>
</thead>
);
}

View File

@@ -0,0 +1,12 @@
import RaidInstanceHeader from "../RaidInstanceHeader";
import RaidInstanceCreator from "./RaidInstanceCreator";
export default function RaidInstanceCreatorUI(){
return (
<>
<RaidInstanceHeader/>
<RaidInstanceCreator/>
</>
);
}

View File

@@ -0,0 +1,64 @@
import DangerButton from "@/components/button/DangerButton";
import SecondaryButton from "@/components/button/SecondaryButton";
import RaidBuilderModal from "@/components/modal/RaidBuilderModal";
import { useDeleteRaidInstance } from "@/hooks/RaidInstanceHooks";
import { RaidInstance } from "@/interface/RaidInstance";
import { useTimedModal } from "@/providers/TimedModalProvider";
import { useEffect } from "react";
export default function DeleteRaidInstanceModal({
display,
close,
raidInstance
}:{
display: boolean;
close: () => void;
raidInstance?: RaidInstance;
}){
const deleteRaidInstanceMutate = useDeleteRaidInstance(raidInstance?.raidGroupId ?? "", raidInstance?.raidInstanceId ?? "");
const { addSuccessMessage, addErrorMessage } = useTimedModal();
const deleteRaidInstance = () => {
deleteRaidInstanceMutate.mutate();
}
useEffect(() => {
if(deleteRaidInstanceMutate.status === "success"){
deleteRaidInstanceMutate.reset();
addSuccessMessage("Raid Instance Deleted");
close();
}
else if(deleteRaidInstanceMutate.status === "error"){
deleteRaidInstanceMutate.reset();
addErrorMessage(`Error deleting raid instance ${raidInstance?.raidInstanceName}: ${deleteRaidInstanceMutate.error.message}`);
console.log(deleteRaidInstanceMutate.error);
}
});
return (
<RaidBuilderModal
display={display}
close={close}
modalHeader={`Delete Raid Instance`}
modalBody={`Are you sure you want to delete ${raidInstance?.raidInstanceName}`}
modalFooter={
<>
<DangerButton
onClick={deleteRaidInstance}
>
Delete
</DangerButton>
<SecondaryButton
onClick={close}
>
Cancel
</SecondaryButton>
</>
}
/>
);
}

View File

@@ -0,0 +1,156 @@
import PrimaryButton from "@/components/button/PrimaryButton";
import SecondaryButton from "@/components/button/SecondaryButton";
import DateInput from "@/components/input/DateInput";
import TextInput from "@/components/input/TextInput";
import RaidBuilderModal from "@/components/modal/RaidBuilderModal";
import { useCreateRaidInstance, useUpdateRaidInstance } from "@/hooks/RaidInstanceHooks";
import { RaidGroup } from "@/interface/RaidGroup";
import { RaidInstance } from "@/interface/RaidInstance";
import { useTimedModal } from "@/providers/TimedModalProvider";
import moment from "moment";
import { useEffect, useState } from "react";
export default function RaidInstanceModal({
display,
close,
raidInstance,
raidGroup
}:{
display: boolean;
close: () => void;
raidInstance?: RaidInstance;
raidGroup: RaidGroup;
}){
const [raidInstanceName, setRaidInstanceName] = useState("");
const [raidStartDate, setRaidStartDate] = useState(new Date());
const [raidEndDate, setRaidEndDate] = useState(new Date());
const [raidSize, setRaidSize] = useState(3);
const [numberRuns, setNumberRuns] = useState(1);
const modalId = crypto.randomUUID().replaceAll("-", "");
const createRaidInstanceMutate = useCreateRaidInstance(raidGroup.raidGroupId ?? "");
const updateRaidInstanceMutate = useUpdateRaidInstance(raidGroup.raidGroupId ?? "");
const { addSuccessMessage, addErrorMessage } = useTimedModal();
useEffect(() => {
if(raidInstance){
setRaidInstanceName(raidInstance.raidInstanceName ?? "");
setRaidStartDate(raidInstance.raidStartDate);
setRaidEndDate(raidInstance.raidEndDate);
setRaidSize(raidInstance.raidSize ?? 0);
setNumberRuns(raidInstance.numberRuns);
}
else{
const currentDate = new Date();
const futureDate = new Date();
futureDate.setHours(futureDate.getHours() + 1);
setRaidInstanceName("");
setRaidStartDate(currentDate);
setRaidEndDate(futureDate);
setRaidSize(3);
setNumberRuns(1);
}
}, [ display, raidInstance ]);
useEffect(() => {
if(createRaidInstanceMutate.status === "success"){
createRaidInstanceMutate.reset();
addSuccessMessage("Raid Instance Created");
close();
}
else if(createRaidInstanceMutate.status === "error"){
createRaidInstanceMutate.reset();
addErrorMessage(`Error creating raid instance ${raidInstanceName}: ${createRaidInstanceMutate.error.message}`);
console.log(createRaidInstanceMutate.error);
}
else if(updateRaidInstanceMutate.status === "success"){
updateRaidInstanceMutate.reset();
addSuccessMessage("Raid Instance Updated");
close();
}
else if(updateRaidInstanceMutate.status === "error"){
updateRaidInstanceMutate.reset();
addErrorMessage(`Error updating raid instance ${raidInstanceName}: ${updateRaidInstanceMutate.error.message}`);
console.log(updateRaidInstanceMutate.error);
}
});
const createRaidInstance = () => {
createRaidInstanceMutate.mutate({
raidInstanceName,
raidStartDate,
raidEndDate,
raidSize,
numberRuns,
raidGroupId: raidGroup.raidGroupId ?? ""
});
}
const updateRaidInstance = () => {
updateRaidInstanceMutate.mutate({
raidInstanceId: raidInstance?.raidInstanceId,
raidInstanceName,
raidStartDate,
raidEndDate,
raidSize,
numberRuns,
raidGroupId: raidGroup.raidGroupId ?? ""
});
}
return (
<RaidBuilderModal
display={display}
close={close}
modalHeader={raidInstance ? "Update Raid Instance" : "Create Raid Instance"}
modalBody={
<div
className="flex flex-col items-center justify-center gap-4"
>
<TextInput
id={`raidInstanceName${modalId}`}
placeholder="Raid Instance Name"
value={raidInstanceName}
onChange={(e) => setRaidInstanceName(e.target.value)}
/>
<br/>
{/*
<NumberInput
id="raidSizeInput"
min={1}
max={100}
value={raidSize}
onChange={setRaidSize}
/>
*/}
<DateInput
id="raidStartDateInput"
value={moment(raidStartDate).format("YYYY-MM-DDTHH:mm")}
onChange={(e) => setRaidStartDate(moment(e.target.value).toDate())}
/>
<DateInput
id="raidEndDateInput"
value={moment(raidEndDate).format("YYYY-MM-DDTHH:mm")}
onChange={(e) => setRaidEndDate(moment(e.target.value).toDate())}
/>
</div>
}
modalFooter={
<>
<PrimaryButton
onClick={raidInstance ? updateRaidInstance : createRaidInstance}
>
{raidInstance ? "Update" : "Create"}
</PrimaryButton>
<SecondaryButton
onClick={close}
>
Cancel
</SecondaryButton>
</>
}
/>
);
}