Raid Instance Creator working
This commit is contained in:
@@ -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;
|
const { id, placeholder, name, inputClasses, labelClasses, accepted } = props;
|
||||||
|
delete props.inputClasses;
|
||||||
|
delete props.labelClasses;
|
||||||
|
delete props.accepted;
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
76
src/components/personCharacter/PersonCharacterSelector.tsx
Normal file
76
src/components/personCharacter/PersonCharacterSelector.tsx
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
className="flex flex-row flex-wrap items-center justify-center space-x-8"
|
||||||
|
style={{flex: "0 0 33.333333333%"}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
personCharacters.map((ch) => (
|
||||||
|
<div
|
||||||
|
key={ch.personCharacterId}
|
||||||
|
className="flex flex-row flex-nowrap"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id={`personCharacter${selectorId}Selector`}
|
||||||
|
name="character"
|
||||||
|
value={ch.personCharacterId}
|
||||||
|
checked={ch.personCharacterId === currentlySelectedCharacterId}
|
||||||
|
onChange={() => {}}
|
||||||
|
onClick={() => updateInput(ch.personCharacterId)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="ml-2"
|
||||||
|
htmlFor={`personCharacter${selectorId}Selector`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex flex-row flex-nowrap"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
<img
|
||||||
|
className="mr-2 max-h-8 max-w-8"
|
||||||
|
src={`${import.meta.env.VITE_ICON_URL}/gameClass/id/${ch.gameClassId}`}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{ch.characterName}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import NumberInput from "../input/NumberInput";
|
||||||
|
|
||||||
export default function RatingSelector({
|
export default function RatingSelector({
|
||||||
rating,
|
rating,
|
||||||
@@ -8,6 +9,7 @@ export default function RatingSelector({
|
|||||||
onChange?: (rating?: number) => void;
|
onChange?: (rating?: number) => void;
|
||||||
}){
|
}){
|
||||||
const ratings = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
const ratings = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||||
|
const selectorId = crypto.randomUUID().replaceAll("-", "");
|
||||||
const [ currentRating, setCurrentRating ] = useState(rating);
|
const [ currentRating, setCurrentRating ] = useState(rating);
|
||||||
|
|
||||||
|
|
||||||
@@ -20,6 +22,19 @@ export default function RatingSelector({
|
|||||||
}, [ currentRating, onChange ]);
|
}, [ currentRating, onChange ]);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<NumberInput
|
||||||
|
id={`characterRatingSelector${selectorId}`}
|
||||||
|
label="Rating"
|
||||||
|
value={currentRating}
|
||||||
|
onChange={(value) => setCurrentRating(value)}
|
||||||
|
min={0}
|
||||||
|
max={10}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<label>
|
<label>
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ export function useGetClassGroups(raidGroupId: string, page: number, pageSize: n
|
|||||||
}
|
}
|
||||||
|
|
||||||
return response.data as ClassGroup[];
|
return response.data as ClassGroup[];
|
||||||
}
|
},
|
||||||
|
enabled: !!raidGroupId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +68,7 @@ export function useGetClassGroupsByRaidLayout(raidGroupId: string, raidLayoutId:
|
|||||||
|
|
||||||
return response.data as (ClassGroup | null)[];
|
return response.data as (ClassGroup | null)[];
|
||||||
},
|
},
|
||||||
enabled: !!raidLayoutId
|
enabled: !!raidGroupId && !!raidLayoutId
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function useGetGameClasses(gameId: string, page: number, pageSize: number
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGetGameClassesByClassGroup(classGroupId: string){
|
export function useGetGameClassesByClassGroup(classGroupId?: string){
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["gameClasses", "classGroups", classGroupId],
|
queryKey: ["gameClasses", "classGroups", classGroupId],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
|||||||
@@ -73,6 +73,25 @@ export function useGetPersonCharactersCountByPersonIdSearch(personId: string, ra
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useGetPersonCharactersByRaidGroup(raidGroupId: string){
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["personCharacters", raidGroupId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await api.get(`/raidGroup/${raidGroupId}/person/character`);
|
||||||
|
|
||||||
|
if(response.status !== 200){
|
||||||
|
throw new Error("Failed to get person characters");
|
||||||
|
}
|
||||||
|
else if(response.data.errors){
|
||||||
|
throw new Error(response.data.errors.join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data as PersonCharacter[];
|
||||||
|
},
|
||||||
|
enabled: !!raidGroupId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useCreatePersonCharacter(raidGroupId: string, personId: string){
|
export function useCreatePersonCharacter(raidGroupId: string, personId: string){
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { RaidInstance } from "@/interface/RaidInstance";
|
import { RaidInstance } from "@/interface/RaidInstance";
|
||||||
|
import { RaidInstancePersonCharacterXref } from "@/interface/RaidInstancePersonCharacterXref";
|
||||||
import { api } from "@/util/AxiosUtil";
|
import { api } from "@/util/AxiosUtil";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
@@ -100,8 +101,6 @@ export function useUpdateRaidInstance(raidGroupId: string){
|
|||||||
return useMutation({
|
return useMutation({
|
||||||
mutationKey: ["updateRaidInstance", raidGroupId],
|
mutationKey: ["updateRaidInstance", raidGroupId],
|
||||||
mutationFn: async (raidInstance: RaidInstance) => {
|
mutationFn: async (raidInstance: RaidInstance) => {
|
||||||
console.log("raidInstance");
|
|
||||||
console.log(raidInstance);
|
|
||||||
const response = await api.put(`/raidGroup/${raidGroupId}/raidInstance/${raidInstance.raidInstanceId}`, raidInstance);
|
const response = await api.put(`/raidGroup/${raidGroupId}/raidInstance/${raidInstance.raidInstanceId}`, raidInstance);
|
||||||
|
|
||||||
if(response.status !== 200){
|
if(response.status !== 200){
|
||||||
@@ -117,6 +116,22 @@ export function useUpdateRaidInstance(raidGroupId: string){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useUpdateRaidInstanceNoInvalidation(raidGroupId: string){
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["updateRaidInstance", raidGroupId],
|
||||||
|
mutationFn: async (raidInstance: RaidInstance) => {
|
||||||
|
const response = await api.put(`/raidGroup/${raidGroupId}/raidInstance/${raidInstance.raidInstanceId}`, raidInstance);
|
||||||
|
|
||||||
|
if(response.status !== 200){
|
||||||
|
throw new Error("Failed to update raid instance");
|
||||||
|
}
|
||||||
|
else if(response.data.errors){
|
||||||
|
throw new Error(response.data.errors.join(", "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useDeleteRaidInstance(raidGroupId: string, raidInstanceId: string){
|
export function useDeleteRaidInstance(raidGroupId: string, raidInstanceId: string){
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -138,3 +153,46 @@ export function useDeleteRaidInstance(raidGroupId: string, raidInstanceId: strin
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function useGetRaidInstancePersonCharacterXrefs(raidGroupId?: string, raidInstanceId?: string){
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["raidInstancePersonCharacterXrefs", raidGroupId, raidInstanceId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await api.get(`/raidGroup/${raidGroupId}/raidInstance/${raidInstanceId}/personCharacterXref`);
|
||||||
|
|
||||||
|
if(response.status !== 200){
|
||||||
|
throw new Error("Failed to get raid instance person character xrefs");
|
||||||
|
}
|
||||||
|
else if(response.data.errors){
|
||||||
|
throw new Error(response.data.errors.join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data as RaidInstancePersonCharacterXref[];
|
||||||
|
},
|
||||||
|
enabled: !!raidGroupId && !!raidInstanceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateRaidInstancePersonCharacterXrefs(raidGroupId?: string, raidInstanceId?: string){
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ["updateRaidInstancePersonCharacterXrefs", raidGroupId, raidInstanceId],
|
||||||
|
mutationFn: async (raidInstancePersonCharacterXrefs: RaidInstancePersonCharacterXref[]) => {
|
||||||
|
const response = await api.post(`/raidGroup/${raidGroupId}/raidInstance/${raidInstanceId}/personCharacterXref`, raidInstancePersonCharacterXrefs);
|
||||||
|
|
||||||
|
if(response.status !== 200){
|
||||||
|
throw new Error("Failed to update raid instance person character xrefs");
|
||||||
|
}
|
||||||
|
else if(response.data.errors){
|
||||||
|
throw new Error(response.data.errors.join(", "));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["raidInstancePersonCharacterXrefs", raidGroupId, raidInstanceId] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,25 @@ import { api } from "@/util/AxiosUtil";
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
|
||||||
|
export function useGetRaidLayout(raidGroupId?: string, raidLayoutId?: string){
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["raidLayout", raidGroupId, raidLayoutId],
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await api.get(`/raidGroup/${raidGroupId}/raidLayout/${raidLayoutId}`);
|
||||||
|
|
||||||
|
if(response.status !== 200){
|
||||||
|
throw new Error("Failed to get raid layout");
|
||||||
|
}
|
||||||
|
else if(response.data.errors){
|
||||||
|
throw new Error(response.data.errors.join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data as RaidLayout;
|
||||||
|
},
|
||||||
|
enabled: !!raidGroupId && !!raidLayoutId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useGetRaidLayoutsByRaidGroup(raidGroupId: string, page: number, pageSize: number, searchTerm?: string){
|
export function useGetRaidLayoutsByRaidGroup(raidGroupId: string, page: number, pageSize: number, searchTerm?: string){
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ["raidLayouts", raidGroupId, {page, pageSize, searchTerm}],
|
queryKey: ["raidLayouts", raidGroupId, {page, pageSize, searchTerm}],
|
||||||
@@ -127,4 +146,3 @@ export function useDeleteRaidLayout(raidGroupId: string, raidLayoutId: string){
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
src/interface/GridLocation.ts
Normal file
4
src/interface/GridLocation.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface GridLocation {
|
||||||
|
row: number;
|
||||||
|
col: number;
|
||||||
|
}
|
||||||
7
src/interface/RaidInstancePersonCharacterXref.ts
Normal file
7
src/interface/RaidInstancePersonCharacterXref.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export interface RaidInstancePersonCharacterXref {
|
||||||
|
raidInstancePersonCharacterXrefId?: string;
|
||||||
|
raidInstanceId: string;
|
||||||
|
personCharacterId: string;
|
||||||
|
groupNumber: number;
|
||||||
|
positionNumber: number;
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import RaidGroupHeader from "@/ui/calendar/RaidGroupHeader";
|
|||||||
import ClassGroupsTab from "@/ui/classGroup/ClassGroupsTab";
|
import ClassGroupsTab from "@/ui/classGroup/ClassGroupsTab";
|
||||||
import PersonTab from "@/ui/person/PersonTab";
|
import PersonTab from "@/ui/person/PersonTab";
|
||||||
import RaidGroupRequestTab from "@/ui/raidGroupRequest/RaidGroupRequestTab";
|
import RaidGroupRequestTab from "@/ui/raidGroupRequest/RaidGroupRequestTab";
|
||||||
import RaidInstanceTab from "@/ui/raidInstances/RaidInstanceTab";
|
import RaidInstanceTab from "@/ui/raidInstance/RaidInstanceTab";
|
||||||
import RaidLayoutTab from "@/ui/raidLayout/RaidLayoutTab";
|
import RaidLayoutTab from "@/ui/raidLayout/RaidLayoutTab";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Navigate, useParams } from "react-router";
|
import { Navigate, useParams } from "react-router";
|
||||||
|
|||||||
@@ -1,10 +1,40 @@
|
|||||||
|
import PrimaryButton from "@/components/button/PrimaryButton";
|
||||||
|
import RaidInstanceLayoutProvider from "@/providers/RaidInstanceLayoutProvider";
|
||||||
|
import RaidInstanceCreatorUI from "@/ui/raidInstance/creator/RaidInstanceCreatorUI";
|
||||||
|
import { BsArrowLeft } from "react-icons/bs";
|
||||||
|
import { useNavigate, useParams } from "react-router";
|
||||||
|
|
||||||
|
|
||||||
export default function RaidInstancePage(){
|
export default function RaidInstancePage(){
|
||||||
//TODO:
|
const { raidGroupId, raidInstanceId } = useParams();
|
||||||
|
const navigator = useNavigate();
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<RaidInstanceLayoutProvider
|
||||||
Raid Instance Page
|
raidGroupId={raidGroupId ?? ""}
|
||||||
|
raidInstanceId={raidInstanceId ?? ""}
|
||||||
|
>
|
||||||
|
<main
|
||||||
|
className="flex flex-col items-center justify-center"
|
||||||
|
>
|
||||||
|
{/* Back to Raid Group Button */}
|
||||||
|
<div
|
||||||
|
className="flex flex-row items-center justify-start w-full my-8"
|
||||||
|
>
|
||||||
|
<PrimaryButton
|
||||||
|
shape="square"
|
||||||
|
onClick={() => navigator(`/raidGroup/${raidGroupId}`)}
|
||||||
|
>
|
||||||
|
<BsArrowLeft
|
||||||
|
size={22}
|
||||||
|
/>
|
||||||
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* UI */}
|
||||||
|
<RaidInstanceCreatorUI/>
|
||||||
|
</main>
|
||||||
|
</RaidInstanceLayoutProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
193
src/providers/RaidInstanceLayoutProvider.tsx
Normal file
193
src/providers/RaidInstanceLayoutProvider.tsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import { useGetClassGroups, useGetClassGroupsByRaidLayout } from "@/hooks/ClassGroupHooks";
|
||||||
|
import { useGetPersonCharactersByRaidGroup } from "@/hooks/PersonCharacterHooks";
|
||||||
|
import { useGetPeopleByRaidGroup } from "@/hooks/PersonHooks";
|
||||||
|
import { useGetRaidGroup } from "@/hooks/RaidGroupHooks";
|
||||||
|
import { useGetRaidInstance, useGetRaidInstancePersonCharacterXrefs } from "@/hooks/RaidInstanceHooks";
|
||||||
|
import { useGetRaidLayout, useGetRaidLayoutsByRaidGroup } from "@/hooks/RaidLayoutHooks";
|
||||||
|
import { ClassGroup } from "@/interface/ClassGroup";
|
||||||
|
import { Person } from "@/interface/Person";
|
||||||
|
import { PersonCharacter } from "@/interface/PersonCharacter";
|
||||||
|
import { RaidGroup } from "@/interface/RaidGroup";
|
||||||
|
import { RaidInstance } from "@/interface/RaidInstance";
|
||||||
|
import { RaidInstancePersonCharacterXref } from "@/interface/RaidInstancePersonCharacterXref";
|
||||||
|
import { RaidLayout } from "@/interface/RaidLayout";
|
||||||
|
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
interface RaidInstanceContextState {
|
||||||
|
raidInstance?: RaidInstance;
|
||||||
|
setRaidInstance: React.Dispatch<React.SetStateAction<RaidInstance | undefined>>;
|
||||||
|
raidGroup?: RaidGroup;
|
||||||
|
classGroups: ClassGroup[];
|
||||||
|
selectedClassGroups: (ClassGroup | null)[];
|
||||||
|
setSelectedClassGroups: React.Dispatch<React.SetStateAction<(ClassGroup | null)[]>>;
|
||||||
|
raidLayout?: RaidLayout;
|
||||||
|
setRaidLayout: React.Dispatch<React.SetStateAction<RaidLayout | undefined>>;
|
||||||
|
raidLayouts: RaidLayout[];
|
||||||
|
people: Person[];
|
||||||
|
roster: Person[];
|
||||||
|
setRoster: React.Dispatch<React.SetStateAction<Person[]>>;
|
||||||
|
personCharacters: PersonCharacter[];
|
||||||
|
setPersonCharacters: React.Dispatch<React.SetStateAction<PersonCharacter[]>>;
|
||||||
|
personCharacterXrefs: RaidInstancePersonCharacterXref[];
|
||||||
|
setPersonCharacterXrefs: React.Dispatch<React.SetStateAction<RaidInstancePersonCharacterXref[]>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RaidInstanceContext = createContext<RaidInstanceContextState>({
|
||||||
|
raidInstance: {} as RaidInstance,
|
||||||
|
setRaidInstance: () => {},
|
||||||
|
raidGroup: {} as RaidGroup,
|
||||||
|
classGroups: [],
|
||||||
|
selectedClassGroups: [],
|
||||||
|
setSelectedClassGroups: () => {},
|
||||||
|
raidLayout: {} as RaidLayout,
|
||||||
|
setRaidLayout: () => {},
|
||||||
|
raidLayouts: [],
|
||||||
|
people: [],
|
||||||
|
roster: [],
|
||||||
|
setRoster: () => {},
|
||||||
|
personCharacters: [],
|
||||||
|
setPersonCharacters: () => {},
|
||||||
|
personCharacterXrefs: [],
|
||||||
|
setPersonCharacterXrefs: () => {}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default function RaidInstanceLayoutProvider({
|
||||||
|
children,
|
||||||
|
raidGroupId,
|
||||||
|
raidInstanceId
|
||||||
|
}:{
|
||||||
|
children: React.ReactNode;
|
||||||
|
raidGroupId: string;
|
||||||
|
raidInstanceId?: string;
|
||||||
|
}){
|
||||||
|
const [ raidInstance, setRaidInstance ] = useState<RaidInstance>();
|
||||||
|
const [ raidGroup, setRaidGroup ] = useState<RaidGroup>();
|
||||||
|
const [ classGroups, setClassGroups ] = useState<ClassGroup[]>([]);
|
||||||
|
const [ raidLayout, setRaidLayout ] = useState<RaidLayout>();
|
||||||
|
const [ raidLayouts, setRaidLayouts ] = useState<RaidLayout[]>([]);
|
||||||
|
const [ people, setPeople ] = useState<Person[]>([]);
|
||||||
|
const [ roster, setRoster ] = useState<Person[]>([]);
|
||||||
|
const [ selectedClassGroups, setSelectedClassGroups ] = useState<(ClassGroup | null)[]>([]);
|
||||||
|
const [ personCharacters, setPersonCharacters ] = useState<PersonCharacter[]>([]);
|
||||||
|
const [ personCharacterXrefs, setPersonCharacterXrefs ] = useState<RaidInstancePersonCharacterXref[]>([]);
|
||||||
|
|
||||||
|
|
||||||
|
const raidGroupQuery = useGetRaidGroup(raidGroupId, false);
|
||||||
|
const classGroupsQuery = useGetClassGroups(raidGroupId, 0, 1000);
|
||||||
|
const raidLayoutsQuery = useGetRaidLayoutsByRaidGroup(raidGroupId, 0, 1000);
|
||||||
|
const peopleQuery = useGetPeopleByRaidGroup(raidGroupId, 0, 1000);
|
||||||
|
const raidInstanceQuery = useGetRaidInstance(raidInstanceId ?? "", raidGroupId);
|
||||||
|
const selectedClassGroupsQuery = useGetClassGroupsByRaidLayout(raidGroupId, raidLayout?.raidLayoutId);
|
||||||
|
const raidLayoutQuery = useGetRaidLayout(raidGroupId, raidInstance?.raidLayoutId);
|
||||||
|
const personCharactersQuery = useGetPersonCharactersByRaidGroup(raidGroupId);
|
||||||
|
const personCharacterXrefsQuery = useGetRaidInstancePersonCharacterXrefs(raidGroupId, raidInstanceId);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//console.log("hit 1");
|
||||||
|
if(raidGroupQuery.status === "success"){
|
||||||
|
setRaidGroup(raidGroupQuery.data);
|
||||||
|
}
|
||||||
|
}, [ raidGroupQuery.status, raidGroupQuery.data ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//console.log("hit 2");
|
||||||
|
if(classGroupsQuery.status === "success"){
|
||||||
|
setClassGroups(classGroupsQuery.data);
|
||||||
|
}
|
||||||
|
}, [ classGroupsQuery.status, classGroupsQuery.data ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//console.log("hit 3");
|
||||||
|
if(raidLayoutsQuery.status === "success"){
|
||||||
|
setRaidLayouts(raidLayoutsQuery.data);
|
||||||
|
}
|
||||||
|
}, [ raidLayoutsQuery.status, raidLayoutsQuery.data ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//console.log("hit 4");
|
||||||
|
if(peopleQuery.status === "success"){
|
||||||
|
setPeople(peopleQuery.data);
|
||||||
|
setRoster(peopleQuery.data);
|
||||||
|
}
|
||||||
|
}, [ peopleQuery.status, peopleQuery.data ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//console.log("hit 5");
|
||||||
|
if(raidInstanceId && raidInstanceQuery.status === "success"){
|
||||||
|
setRaidInstance(raidInstanceQuery.data);
|
||||||
|
if(!raidInstanceQuery.data.raidLayoutId){
|
||||||
|
const newClassGroups: (ClassGroup | null)[] = [];
|
||||||
|
for(let cnt = 0;cnt < (raidInstanceQuery.data.raidSize ?? 0);++cnt){
|
||||||
|
newClassGroups.push(null);
|
||||||
|
}
|
||||||
|
setSelectedClassGroups(newClassGroups);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if(!raidInstanceId){
|
||||||
|
setRaidInstance({} as RaidInstance);
|
||||||
|
}
|
||||||
|
}, [ raidInstanceId, raidInstanceQuery.status, raidInstanceQuery.data ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//console.log("hit 6");
|
||||||
|
if(selectedClassGroupsQuery.status === "success"){
|
||||||
|
setSelectedClassGroups(selectedClassGroupsQuery.data);
|
||||||
|
}
|
||||||
|
}, [ selectedClassGroupsQuery.status, selectedClassGroupsQuery.data ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//console.log("hit 7");
|
||||||
|
if(personCharactersQuery.status === "success"){
|
||||||
|
setPersonCharacters(personCharactersQuery.data);
|
||||||
|
}
|
||||||
|
}, [ personCharactersQuery.status, personCharactersQuery.data ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//console.log("hit 8");
|
||||||
|
if(personCharacterXrefsQuery.status === "success"){
|
||||||
|
setPersonCharacterXrefs(personCharacterXrefsQuery.data);
|
||||||
|
}
|
||||||
|
}, [ personCharacterXrefsQuery.status, personCharacterXrefsQuery.data, people ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
//console.log("hit 9");
|
||||||
|
if(raidLayoutQuery.status === "success"){
|
||||||
|
setRaidLayout(raidLayoutQuery.data);
|
||||||
|
}
|
||||||
|
}, [ raidLayoutQuery.status, raidLayoutQuery.data ]);
|
||||||
|
|
||||||
|
|
||||||
|
const currentValue: RaidInstanceContextState = useMemo(() => ({
|
||||||
|
raidInstance, setRaidInstance,
|
||||||
|
raidGroup,
|
||||||
|
classGroups,
|
||||||
|
selectedClassGroups, setSelectedClassGroups,
|
||||||
|
raidLayout, setRaidLayout,
|
||||||
|
raidLayouts,
|
||||||
|
people,
|
||||||
|
roster, setRoster,
|
||||||
|
personCharacters, setPersonCharacters,
|
||||||
|
personCharacterXrefs, setPersonCharacterXrefs
|
||||||
|
}), [ classGroups, selectedClassGroups, people, raidGroup, raidInstance, raidLayout, raidLayouts, roster, personCharacters, personCharacterXrefs ]);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RaidInstanceContext.Provider value={currentValue}>
|
||||||
|
{children}
|
||||||
|
</RaidInstanceContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function useRaidInstanceContext(){
|
||||||
|
const context = useContext(RaidInstanceContext);
|
||||||
|
|
||||||
|
if(!context){
|
||||||
|
throw new Error("useRaidInstanceContext must be used within a RaidInstanceLayoutProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -69,7 +69,7 @@ export function TimedModalProvider({
|
|||||||
<Modal
|
<Modal
|
||||||
display={display}
|
display={display}
|
||||||
backgroundType="none"
|
backgroundType="none"
|
||||||
className="bg-neutral-300 dark:bg-neutral-600"
|
className="bg-neutral-300 dark:bg-neutral-600 rounded-t-none"
|
||||||
top={true}
|
top={true}
|
||||||
>
|
>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ export default function SelectClassGroupModal({
|
|||||||
}:{
|
}:{
|
||||||
display: boolean;
|
display: boolean;
|
||||||
close: () => void;
|
close: () => void;
|
||||||
selectedClassGroup?: ClassGroup | null;
|
selectedClassGroup?: ClassGroup | null | undefined;
|
||||||
onChange: (classGroup?: ClassGroup | null) => void;
|
onChange: (classGroup?: ClassGroup | null | undefined) => void;
|
||||||
raidGroupId: string;
|
raidGroupId: string;
|
||||||
}){
|
}){
|
||||||
const [ currentClassGroup, setCurrentClassGroup ] = useState(selectedClassGroup);
|
const [ currentClassGroup, setCurrentClassGroup ] = useState(selectedClassGroup);
|
||||||
|
|||||||
137
src/ui/person/modals/PersonSelectorModal.tsx
Normal file
137
src/ui/person/modals/PersonSelectorModal.tsx
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
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 { Person } from "@/interface/Person";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
export default function PersonSelectorModal({
|
||||||
|
display,
|
||||||
|
close,
|
||||||
|
onSubmit,
|
||||||
|
people,
|
||||||
|
selectedPersonIds
|
||||||
|
}:{
|
||||||
|
display: boolean;
|
||||||
|
close: () => void;
|
||||||
|
onSubmit: (personIds: string[]) => void;
|
||||||
|
people: Person[];
|
||||||
|
selectedPersonIds: string[];
|
||||||
|
}){
|
||||||
|
const pageSize = 16;
|
||||||
|
const modalId = crypto.randomUUID().replaceAll("-", "");
|
||||||
|
const [ page, setPage ] = useState(1);
|
||||||
|
const [ searchTerm, setSearchTerm ] = useState<string>();
|
||||||
|
const [ currentlySelectedPersonIds, setCurrentlySelectedPersonIds ] = useState(selectedPersonIds);
|
||||||
|
const [ matchingPeople, setMatchingPeople ] = useState(people);
|
||||||
|
const [ currentlyVisiblePeople, setCurrentlyVisiblePeople ] = useState(people.slice((page - 1) * pageSize, page * pageSize));
|
||||||
|
|
||||||
|
|
||||||
|
const updateInput = (personId: string) => {
|
||||||
|
if(currentlySelectedPersonIds.includes(personId)){
|
||||||
|
setCurrentlySelectedPersonIds(currentlySelectedPersonIds.filter((id) => id !== personId));
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
setCurrentlySelectedPersonIds([...currentlySelectedPersonIds, personId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Update selected when the modal is updated
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentlySelectedPersonIds(selectedPersonIds);
|
||||||
|
}, [ selectedPersonIds ]);
|
||||||
|
|
||||||
|
//Update page data when modal becomes visible
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(1);
|
||||||
|
setSearchTerm(undefined);
|
||||||
|
}, [ display ]);
|
||||||
|
|
||||||
|
//Update visible people when paging info is updated
|
||||||
|
useEffect(() => {
|
||||||
|
//! Update visible people
|
||||||
|
const filteredPeople = people.filter((person) => person.personName.toLowerCase().includes(searchTerm?.toLowerCase() ?? ""));
|
||||||
|
|
||||||
|
setMatchingPeople(filteredPeople);
|
||||||
|
setCurrentlyVisiblePeople(filteredPeople.slice((page - 1) * pageSize, page * pageSize));
|
||||||
|
}, [ page, pageSize, searchTerm, people ]);
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RaidBuilderModal
|
||||||
|
display={display}
|
||||||
|
close={close}
|
||||||
|
modalHeader="Select Raid Layout"
|
||||||
|
modalBody={
|
||||||
|
<div
|
||||||
|
className="flex flex-col items-center justify-center w-full gap-y-8"
|
||||||
|
>
|
||||||
|
{/* Search Box */}
|
||||||
|
<div>
|
||||||
|
<TextInput
|
||||||
|
id={`raidLayoutSelectorModalSearchBox${modalId}`}
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
placeholder="Search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Raid Layouts */}
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-4 gap-4"
|
||||||
|
style={{flex: "0 0 33.333333333%"}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
currentlyVisiblePeople.map((person) => (
|
||||||
|
<div
|
||||||
|
key={person.personId}
|
||||||
|
className="flex flex-row"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id={`personModal${modalId}`}
|
||||||
|
type="checkbox"
|
||||||
|
name="personId"
|
||||||
|
value={person.personId}
|
||||||
|
checked={currentlySelectedPersonIds.includes(person.personId ?? "")}
|
||||||
|
onChange={() => {}}
|
||||||
|
onClick={() => updateInput(person.personId ?? "")}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="ml-2"
|
||||||
|
htmlFor={`personModal${modalId}`}
|
||||||
|
>
|
||||||
|
{person.personName}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{/* Pagination */}
|
||||||
|
<div>
|
||||||
|
<Pagination
|
||||||
|
currentPage={page}
|
||||||
|
totalPages={Math.ceil(matchingPeople.length / pageSize)}
|
||||||
|
onChange={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
modalFooter={
|
||||||
|
<>
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={() => {onSubmit(currentlySelectedPersonIds); close();}}
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</PrimaryButton>
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</SecondaryButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
177
src/ui/personCharacter/modal/PersonCharacterSelectorModal.tsx
Normal file
177
src/ui/personCharacter/modal/PersonCharacterSelectorModal.tsx
Normal file
@@ -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<PersonCharacter[]>(personCharacters);
|
||||||
|
const [ currentlyVisibleCharacters, setCurrentlyVisibleCharacters ] = useState<PersonCharacter[]>([]);
|
||||||
|
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 (
|
||||||
|
<RaidBuilderModal
|
||||||
|
display={display}
|
||||||
|
close={close}
|
||||||
|
modalHeader="Character Selector"
|
||||||
|
modalBody={
|
||||||
|
<>
|
||||||
|
{/* Search */}
|
||||||
|
<div>
|
||||||
|
<TextInput
|
||||||
|
id={`personCharacterSelectorModal${modalId}SearchTerm`}
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Tabs */}
|
||||||
|
<div
|
||||||
|
className="flex flex-row items-center justify-center my-4"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
Object.values(SelectorTabs).map((tab) => (
|
||||||
|
<div
|
||||||
|
key={tab}
|
||||||
|
className={clsx(
|
||||||
|
"px-4 py-2 mx-2 cursor-pointer rounded-lg",
|
||||||
|
{
|
||||||
|
"bg-blue-400": selectedTab === tab,
|
||||||
|
"bg-blue-600": selectedTab !== tab
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
onClick={() => setSelectedTab(tab)}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{/* Character Selector */}
|
||||||
|
<div
|
||||||
|
className="m-8 max-w-[50rem]"
|
||||||
|
>
|
||||||
|
<PersonCharacterSelector
|
||||||
|
personCharacters={currentlyVisibleCharacters}
|
||||||
|
selectedCharacterId={currentlySelectedCharacterId}
|
||||||
|
onChange={setCurrentlySelectedCharacterId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Pagination */}
|
||||||
|
<div>
|
||||||
|
<Pagination
|
||||||
|
currentPage={page}
|
||||||
|
totalPages={Math.ceil(matchingCharacters.length / pageSize)}
|
||||||
|
onChange={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
modalFooter={
|
||||||
|
<>
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={() => { setInput(currentlySelectedCharacterId); close(); }}
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</PrimaryButton>
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</SecondaryButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
207
src/ui/raidInstance/RaidInstanceHeader.tsx
Normal file
207
src/ui/raidInstance/RaidInstanceHeader.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
129
src/ui/raidInstance/creator/RaidInstanceCreator.tsx
Normal file
129
src/ui/raidInstance/creator/RaidInstanceCreator.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
51
src/ui/raidInstance/creator/RaidInstanceCreatorTable.tsx
Normal file
51
src/ui/raidInstance/creator/RaidInstanceCreatorTable.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
132
src/ui/raidInstance/creator/RaidInstanceCreatorTableBody.tsx
Normal file
132
src/ui/raidInstance/creator/RaidInstanceCreatorTableBody.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
>
|
||||||
|
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
);
|
||||||
|
}
|
||||||
12
src/ui/raidInstance/creator/RaidInstanceCreatorUI.tsx
Normal file
12
src/ui/raidInstance/creator/RaidInstanceCreatorUI.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import RaidInstanceHeader from "../RaidInstanceHeader";
|
||||||
|
import RaidInstanceCreator from "./RaidInstanceCreator";
|
||||||
|
|
||||||
|
|
||||||
|
export default function RaidInstanceCreatorUI(){
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<RaidInstanceHeader/>
|
||||||
|
<RaidInstanceCreator/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -25,8 +25,8 @@ export default function RaidInstanceModal({
|
|||||||
const [raidInstanceName, setRaidInstanceName] = useState("");
|
const [raidInstanceName, setRaidInstanceName] = useState("");
|
||||||
const [raidStartDate, setRaidStartDate] = useState(new Date());
|
const [raidStartDate, setRaidStartDate] = useState(new Date());
|
||||||
const [raidEndDate, setRaidEndDate] = useState(new Date());
|
const [raidEndDate, setRaidEndDate] = useState(new Date());
|
||||||
const [raidSize, setRaidSize] = useState(0);
|
const [raidSize, setRaidSize] = useState(3);
|
||||||
const [numberRuns, setNumberRuns] = useState(0);
|
const [numberRuns, setNumberRuns] = useState(1);
|
||||||
const modalId = crypto.randomUUID().replaceAll("-", "");
|
const modalId = crypto.randomUUID().replaceAll("-", "");
|
||||||
|
|
||||||
const createRaidInstanceMutate = useCreateRaidInstance(raidGroup.raidGroupId ?? "");
|
const createRaidInstanceMutate = useCreateRaidInstance(raidGroup.raidGroupId ?? "");
|
||||||
@@ -48,10 +48,10 @@ export default function RaidInstanceModal({
|
|||||||
setRaidInstanceName("");
|
setRaidInstanceName("");
|
||||||
setRaidStartDate(currentDate);
|
setRaidStartDate(currentDate);
|
||||||
setRaidEndDate(futureDate);
|
setRaidEndDate(futureDate);
|
||||||
setRaidSize(0);
|
setRaidSize(3);
|
||||||
setNumberRuns(0);
|
setNumberRuns(1);
|
||||||
}
|
}
|
||||||
}, [ raidInstance ]);
|
}, [ display, raidInstance ]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(createRaidInstanceMutate.status === "success"){
|
if(createRaidInstanceMutate.status === "success"){
|
||||||
135
src/ui/raidLayout/modal/RaidLayoutSelectorModal.tsx
Normal file
135
src/ui/raidLayout/modal/RaidLayoutSelectorModal.tsx
Normal file
@@ -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<RaidLayout[]>(raidLayouts);
|
||||||
|
const [ currentlyVisibleLayouts, setCurrentlyVisibleLayouts ] = useState<RaidLayout[]>(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 (
|
||||||
|
<RaidBuilderModal
|
||||||
|
display={display}
|
||||||
|
close={close}
|
||||||
|
modalHeader="Select Raid Layout"
|
||||||
|
modalBody={
|
||||||
|
<div
|
||||||
|
className="flex flex-col items-center justify-center w-full gap-y-8"
|
||||||
|
>
|
||||||
|
{/* Search Box */}
|
||||||
|
<div>
|
||||||
|
<TextInput
|
||||||
|
id={`raidLayoutSelectorModalSearchBox${modalId}`}
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
placeholder="Search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/* Raid Layouts */}
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-4 gap-4"
|
||||||
|
style={{flex: "0 0 33.333333333%"}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
currentlyVisibleLayouts.map((raidLayout) => (
|
||||||
|
<div
|
||||||
|
key={raidLayout.raidLayoutId}
|
||||||
|
className="flex flex-row"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id={`raidLayoutModal${modalId}`}
|
||||||
|
type="radio"
|
||||||
|
name="raidLayoutId"
|
||||||
|
value={raidLayout.raidLayoutId}
|
||||||
|
checked={currentlySelectedLayoutId === raidLayout.raidLayoutId}
|
||||||
|
onChange={() => {}}
|
||||||
|
onClick={() => updateInput(raidLayout.raidLayoutId)}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className="ml-2"
|
||||||
|
htmlFor={`raidLayoutModal${modalId}`}
|
||||||
|
>
|
||||||
|
{raidLayout.raidLayoutName}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{/* Pagination */}
|
||||||
|
<div>
|
||||||
|
<Pagination
|
||||||
|
currentPage={page}
|
||||||
|
totalPages={Math.ceil(matchingLayouts.length / pageSize)}
|
||||||
|
onChange={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
modalFooter={
|
||||||
|
<>
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={() => {onSubmit(currentlySelectedLayoutId); close();}}
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</PrimaryButton>
|
||||||
|
<SecondaryButton
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</SecondaryButton>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
src/util/PersonCharacterUtil.ts
Normal file
63
src/util/PersonCharacterUtil.ts
Normal file
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user