People tab working

This commit is contained in:
2025-03-08 13:26:39 -05:00
parent 0dfb971bc2
commit b763a1c7bd
22 changed files with 1050 additions and 22 deletions

View File

@@ -0,0 +1,73 @@
import { useGetGameClasses } from "@/hooks/GameClassHooks";
import { useEffect, useState } from "react";
import DangerMessage from "../message/DangerMessage";
export function GameClassSelector({
gameId,
gameClassId,
onChange
}:{
gameId: string;
gameClassId?: string;
onChange?: (gameClassId?: string) => void;
}){
const [ selectedGameClassId, setSelectedGameClassId ] = useState(gameClassId);
const selectorId = crypto.randomUUID().replaceAll("-", "");
const gameClassesQuery = useGetGameClasses(gameId, 0, 100, undefined);
useEffect(() => {
onChange?.(selectedGameClassId);
}, [ selectedGameClassId, onChange ]);
if(gameClassesQuery.status === "pending"){
return <div>Loading...</div>
}
else if(gameClassesQuery.status === "error"){
return <DangerMessage>Error loading Game Classes: {gameClassesQuery.error.message}</DangerMessage>
}
else{
return (
<div
className="grid grid-cols-3 gap-4"
style={{flex: "0 0 33.333333333%"}}
>
{
gameClassesQuery.data.map((gameClass) => (
<div
key={gameClass.gameClassId}
className="flex flex-row"
>
<input
id={`gameClassSelector${gameClass.gameClassId}${selectorId}`}
className="cursor-pointer"
type="radio"
name="gameClassId"
value={gameClass.gameClassId}
checked={selectedGameClassId === gameClass.gameClassId}
onChange={(e) => setSelectedGameClassId(e.target.value)}
/>
<label
className="ml-2 flex flex-row flex-nowrap justify-center items-center text-nowrap cursor-pointer"
htmlFor={`gameClassSelector${gameClass.gameClassId}${selectorId}`}
>
{
gameClass.gameClassIcon &&
<img
className="m-auto max-h-6 max-w-6 mr-2"
src={`${import.meta.env.VITE_ICON_URL}/gameClass/${gameClass.gameClassIcon}`}
/>
}
{gameClass.gameClassName}
</label>
</div>
))
}
</div>
);
}
}

View File

@@ -0,0 +1,49 @@
import { useGetPersonCharactersByPersonId } from "@/hooks/PersonCharacterHooks";
import DangerMessage from "../message/DangerMessage";
export default function PersonCharacterDisplay({
personId,
raidGroupId
}:{
personId: string;
raidGroupId: string;
}){
const personCharacterQuery = useGetPersonCharactersByPersonId(personId, raidGroupId);
if(personCharacterQuery.status === "pending"){
return (<div>Loading...</div>);
}
else if(personCharacterQuery.status === "error"){
return (<DangerMessage>Error loading characters: {personCharacterQuery.error.message}</DangerMessage>);
}
else{
return (
<div
className="flex flex-row flex-wrap items-center justify-center"
>
{
personCharacterQuery.data.map((personCharacter) => {
return (
<div
key={personCharacter.personCharacterId}
className="flex flex-row flex-nowrap items-center justify-center mr-8"
>
<img
className="m-auto max-h-6 max-w-6 mr-2"
src={`${import.meta.env.VITE_ICON_URL}/gameClass/id/${personCharacter.gameClassId}`}
/>
{personCharacter.characterName}
</div>
);
})
}
{
personCharacterQuery.data.length === 0 &&
<div>No characters</div>
}
</div>
);
}
}

View File

@@ -0,0 +1,43 @@
import { useEffect, useState } from "react";
export default function RatingSelector({
rating,
onChange
}:{
rating?: number;
onChange?: (rating?: number) => void;
}){
const ratings = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const [ currentRating, setCurrentRating ] = useState(rating);
useEffect(() => {
setCurrentRating(rating);
}, [ rating, setCurrentRating ]);
useEffect(() => {
onChange?.(currentRating);
}, [ currentRating, onChange ]);
return (
<div>
<select
onChange={(e) => setCurrentRating(parseInt(e.target.value))}
value={currentRating}
>
<option value={undefined}></option>
{
ratings.map((rating) => (
<option
key={rating}
value={rating}
>
{rating}
</option>
))
}
</select>
</div>
);
}

View File

@@ -0,0 +1,88 @@
import { PersonCharacter } from "@/interface/PersonCharacter";
import { api } from "@/util/AxiosUtil";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export function useGetPersonCharactersByPersonId(personId: string, raidGroupId: string){
return useQuery({
queryKey: ["personCharacters", personId],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}/person/${personId}/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[];
}
});
}
export function useCreatePersonCharacter(raidGroupId: string, personId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["createPersonCharacter"],
mutationFn: async (personCharacter: PersonCharacter) => {
const response = await api.post(`/raidGroup/${raidGroupId}/person/${personId}/character`, personCharacter);
if(response.status !== 200){
throw new Error("Failed to create person character");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["personCharacters"] });
}
});
}
export function useUpdatePersonCharacter(raidGroupId: string, personId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["updatePersonCharacter"],
mutationFn: async (personCharacter: PersonCharacter) => {
const response = await api.put(`/raidGroup/${raidGroupId}/person/${personId}/character/${personCharacter.personCharacterId}`, personCharacter);
if(response.status !== 200){
throw new Error("Failed to update person character");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["personCharacters"] });
}
});
}
export function useDeletePersonCharacter(raidGroupId: string, personId: string){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["deletePersonCharacter"],
mutationFn: async (personCharacterId: string) => {
const response = await api.delete(`/raidGroup/${raidGroupId}/person/${personId}/character/${personCharacterId}`);
if(response.status !== 200){
throw new Error("Failed to delete person character");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["personCharacters"] });
}
});
}

138
src/hooks/PersonHooks.ts Normal file
View File

@@ -0,0 +1,138 @@
import { Person } from "@/interface/Person";
import { api } from "@/util/AxiosUtil";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export function useGetPerson(raidGroupId: string, personId: string, disabled: boolean){
return useQuery({
queryKey: ["people", raidGroupId, personId],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}/person/${personId}`);
if(response.status !== 200){
throw new Error("Failed to get person");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
return response.data as Person;
},
enabled: !disabled
});
}
export function useGetPeopleByRaidGroup(raidGroupId: string, page: number, pageSize: number, searchTerm?: string){
return useQuery({
queryKey: ["people", raidGroupId, { page, pageSize, searchTerm }],
queryFn: async () => {
const searchParams = new URLSearchParams();
searchParams.append("page", page.toString());
searchParams.append("pageSize", pageSize.toString());
if(searchTerm){
searchParams.append("searchTerm", searchTerm);
}
const response = await api.get(`/raidGroup/${raidGroupId}/person?${searchParams}`);
if(response.status !== 200){
throw new Error("Failed to get people");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
return response.data as Person[];
}
});
}
export function useGetPeopleByRaidGroupCount(raidGroupId: string, searchTerm?: string){
const searchParams = new URLSearchParams();
if(searchTerm){
searchParams.append("searchTerm", searchTerm);
}
return useQuery({
queryKey: ["people", raidGroupId, "count", searchTerm],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}/person/count?${searchParams}`);
if(response.status !== 200){
throw new Error("Failed to get people count");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
return response.data.count as number;
}
});
}
export function useCreatePerson(){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["createPerson"],
mutationFn: async ({raidGroupId, personName, discordId}:{raidGroupId: string; personName: string; discordId?: string;}) => {
const response = await api.post(`/raidGroup/${raidGroupId}/person`, {raidGroupId, personName, discordId});
if(response.status !== 200){
throw new Error("Failed to create person");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["people"] });
}
});
}
export function useUpdatePerson(){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["updatePerson"],
mutationFn: async (person: Person) => {
const response = await api.put(`/raidGroup/${person.raidGroupId}/person/${person.personId}`, person);
if(response.status !== 200){
throw new Error("Failed to update person");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["people"] });
}
});
}
export function useDeletePerson(){
const queryClient = useQueryClient();
return useMutation({
mutationKey: ["deletePerson"],
mutationFn: async ({raidGroupId, personId}:{raidGroupId: string; personId: string;}) => {
const response = await api.delete(`/raidGroup/${raidGroupId}/person/${personId}`);
if(response.status !== 200){
throw new Error("Failed to delete person");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["people"] });
}
});
}

6
src/interface/Person.ts Normal file
View File

@@ -0,0 +1,6 @@
export interface Person {
personId?: string;
raidGroupId: string;
personName: string;
discordId?: string;
}

View File

@@ -0,0 +1,8 @@
export interface PersonCharacter {
personCharacterId?: string;
personId: string;
gameClassId: string;
characterName: string;
characterRating?: number;
characterComments?: string;
}

View File

@@ -3,6 +3,7 @@ import { useGetRaidGroup } from "@/hooks/RaidGroupHooks";
import { RaidGroup } from "@/interface/RaidGroup"; import { RaidGroup } from "@/interface/RaidGroup";
import RaidGroupCalendarDisplay from "@/ui/calendar/RaidGroupCalendarDisplay"; import RaidGroupCalendarDisplay from "@/ui/calendar/RaidGroupCalendarDisplay";
import RaidGroupHeader from "@/ui/calendar/RaidGroupHeader"; import RaidGroupHeader from "@/ui/calendar/RaidGroupHeader";
import PersonTab from "@/ui/person/PersonTab";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Navigate, useParams } from "react-router"; import { Navigate, useParams } from "react-router";
@@ -40,6 +41,10 @@ export default function RaidGroupPage(){
{ {
tabHeader: "Calendar", tabHeader: "Calendar",
tabContent: <RaidGroupCalendarDisplay gameId={raidGroup?.gameId ?? ""} raidGroupId={raidGroupId!}/> tabContent: <RaidGroupCalendarDisplay gameId={raidGroup?.gameId ?? ""} raidGroupId={raidGroupId!}/>
},
{
tabHeader: "People",
tabContent: <PersonTab raidGroup={raidGroup}/>
} }
]; ];

View File

@@ -17,7 +17,7 @@ export default function AdminAccountsTab(){
const pageSize = 10; const pageSize = 10;
const modalId = crypto.randomUUID().replaceAll("-", ""); const modalId = crypto.randomUUID().replaceAll("-", "");
const accountsCountQuery = useGetAccountsCount(sentSearchTerm); const accountsCountQuery = useGetAccountsCount(sentSearchTerm);
@@ -53,7 +53,7 @@ export default function AdminAccountsTab(){
className="flex flex-row items-center justify-center w-full" className="flex flex-row items-center justify-center w-full"
> >
<PrimaryButton <PrimaryButton
className="mb-8 tex-tnowrap" className="mb-8 text-nowrap"
onClick={() => setDisplayCreateAccountModal(true)} onClick={() => setDisplayCreateAccountModal(true)}
> >
Create Account Create Account

View File

@@ -31,10 +31,8 @@ export default function RaidGroupHeader({
{ {
raidGroup.raidGroupIcon && raidGroup.raidGroupIcon &&
<img <img
className="m-auto mr-4" className="m-auto mr-4 max-h-18 max-w-18"
src={`${import.meta.env.VITE_ICON_URL}/raidGroupIcons/${raidGroup.raidGroupIcon}`} src={`${import.meta.env.VITE_ICON_URL}/raidGroup/${raidGroup.raidGroupIcon}`}
height={72}
width={72}
/> />
} }
{raidGroup.raidGroupName} {raidGroup.raidGroupName}

View File

@@ -31,10 +31,8 @@ export default function GameHeader({
{ {
game.gameIcon && game.gameIcon &&
<img <img
className="m-auto mr-4" className="m-auto mr-4 max-h-18 max-w-18"
src={`${import.meta.env.VITE_ICON_URL}/gameIcons/${game.gameIcon}`} src={`${import.meta.env.VITE_ICON_URL}/game/${game.gameIcon}`}
height={72}
width={72}
/> />
} }
{game.gameName} {game.gameName}

View File

@@ -47,10 +47,8 @@ export default function GamesList({
className="absolute -my-4" className="absolute -my-4"
> >
<img <img
className="m-auto" className="m-auto max-h-14 max-w-14"
src={`${import.meta.env.VITE_ICON_URL}/gameIcons/${game.gameIcon}`} src={`${import.meta.env.VITE_ICON_URL}/game/${game.gameIcon}`}
height={56}
width={56}
/> />
</div> </div>
} }

View File

@@ -43,13 +43,11 @@ export default function GameClassList({
{ {
gameClass.gameClassIcon && gameClass.gameClassIcon &&
<div <div
className="absolute -my-4" className="absolute -my-4 max-h-14 max-w-14"
> >
<img <img
className="m-auto" className="m-auto"
src={`${import.meta.env.VITE_ICON_URL}/gameClassIcons/${gameClass.gameClassIcon}`} src={`${import.meta.env.VITE_ICON_URL}/gameClass/${gameClass.gameClassIcon}`}
height={56}
width={56}
/> />
</div> </div>
} }

View File

@@ -0,0 +1,51 @@
import { ButtonProps } from "@/components/button/Button";
import DangerButton from "@/components/button/DangerButton";
import PrimaryButton from "@/components/button/PrimaryButton";
import SuccessButton from "@/components/button/SuccessButton";
import { BsPencilFill, BsPlusLg, BsTrash3 } from "react-icons/bs";
export default function PersonAdminButtons({
buttonProps,
showUpdatePersonModal,
showDeletePersonModal,
showCreatePersonCharacterModal
}:{
buttonProps: ButtonProps;
showUpdatePersonModal: () => void;
showDeletePersonModal: () => void;
showCreatePersonCharacterModal: () => void;
}){
return (
<div
className="flex flex-row gap-2"
>
<SuccessButton
{...buttonProps}
size="sm"
onClick={showCreatePersonCharacterModal}
>
<BsPlusLg
size={30}
strokeWidth={1}
/>
</SuccessButton>
<PrimaryButton
{...buttonProps}
onClick={showUpdatePersonModal}
>
<BsPencilFill
size={22}
/>
</PrimaryButton>
<DangerButton
{...buttonProps}
onClick={showDeletePersonModal}
>
<BsTrash3
size={22}
/>
</DangerButton>
</div>
);
}

View File

@@ -0,0 +1,122 @@
import { ButtonProps } from "@/components/button/Button";
import PersonCharacterDisplay from "@/components/personCharacter/PersonCharacterDisplay";
import Table from "@/components/table/Table";
import { Person } from "@/interface/Person";
import { RaidGroup } from "@/interface/RaidGroup";
import { useState } from "react";
import PersonCharacterModal from "../personCharacter/modal/PersonCharacterModal";
import DeletePersonModal from "./modals/DeletePersonModal";
import PersonModal from "./modals/PersonModal";
import PersonAdminButtons from "./PersonAdminButtons";
export default function PersonList({
people,
raidGroup
}:{
people: Person[];
raidGroup: RaidGroup;
}){
const [ selectedPerson, setSelectedPerson ] = useState<Person>();
const [ displayUpdatePersonModal, setDisplayUpdatePersonModal ] = useState(false);
const [ displayDeletePersonModal, setDisplayDeletePersonModal ] = useState(false);
const [ displayCreatePersonCharacterModal, setDisplayCreatePersonCharacterModal ] = useState(false);
const buttonProps: ButtonProps = {
variant: "ghost",
size: "md",
shape: "square"
};
const headElements: React.ReactNode[] = [
<div
className="text-nowrap"
>
Person Name
</div>,
<div
className="text-nowrap"
>
Discord ID
</div>,
<div>
Characters
</div>,
<div
className="pl-16"
>
Actions
</div>
];
const bodyElements: React.ReactNode[][] = people.map((person) => [
<div>
{person.personName}
</div>,
<div>
{person.discordId}
</div>,
<div>
<PersonCharacterDisplay
personId={person.personId ?? ""}
raidGroupId={raidGroup.raidGroupId ?? ""}
/>
</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>
<PersonAdminButtons
buttonProps={buttonProps}
showUpdatePersonModal={() => {
setSelectedPerson(person);
setDisplayUpdatePersonModal(true);
}}
showDeletePersonModal={() => {
setSelectedPerson(person);
setDisplayDeletePersonModal(true);
}}
showCreatePersonCharacterModal={() => {
setSelectedPerson(person);
setDisplayCreatePersonCharacterModal(true);
}}
/>
</div>
]);
return (
<>
<Table
tableHeadElements={headElements}
tableBodyElements={bodyElements}
/>
<PersonModal
display={displayUpdatePersonModal}
close={() => {setDisplayUpdatePersonModal(false); setSelectedPerson(undefined);}}
person={selectedPerson}
raidGroupId={raidGroup.raidGroupId ?? ""}
/>
<DeletePersonModal
display={displayDeletePersonModal}
close={() => {setDisplayDeletePersonModal(false); setSelectedPerson(undefined);}}
person={selectedPerson}
raidGroupId={raidGroup.raidGroupId ?? ""}
/>
<PersonCharacterModal
display={displayCreatePersonCharacterModal}
close={() => {setDisplayCreatePersonCharacterModal(false); setSelectedPerson(undefined);}}
personId={selectedPerson?.personId ?? ""}
personCharacter={undefined}
gameId={raidGroup.gameId}
raidGroupId={raidGroup.raidGroupId ?? ""}
/>
</>
);
}

View File

@@ -0,0 +1,7 @@
export default function PersonListSkeleton(){
return (
<div>
Person List Skeleton
</div>
);
}

View File

@@ -0,0 +1,36 @@
import DangerMessage from "@/components/message/DangerMessage";
import { useGetPeopleByRaidGroup } from "@/hooks/PersonHooks";
import { RaidGroup } from "@/interface/RaidGroup";
import PersonList from "./PersonList";
import PersonListSkeleton from "./PersonListSkeleton";
export default function PersonLoader({
raidGroup,
page,
pageSize,
searchTerm
}:{
raidGroup: RaidGroup;
page: number;
pageSize: number;
searchTerm?: string;
}){
const peopleQuery = useGetPeopleByRaidGroup(raidGroup.raidGroupId ?? "", page - 1, pageSize, searchTerm);
if(peopleQuery.status === "pending"){
return <PersonListSkeleton/>
}
else if(peopleQuery.status === "error"){
return <DangerMessage>Error: {peopleQuery.error.message}</DangerMessage>
}
else{
return (
<PersonList
people={peopleQuery.data ?? []}
raidGroup={raidGroup}
/>
);
}
}

106
src/ui/person/PersonTab.tsx Normal file
View File

@@ -0,0 +1,106 @@
import PrimaryButton from "@/components/button/PrimaryButton";
import TextInput from "@/components/input/TextInput";
import Pagination from "@/components/pagination/Pagination";
import { useGetPeopleByRaidGroupCount } from "@/hooks/PersonHooks";
import { RaidGroup } from "@/interface/RaidGroup";
import { useEffect, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import PersonModal from "./modals/PersonModal";
import PersonLoader from "./PersonLoader";
export default function PersonTab({
raidGroup
}:{
raidGroup: RaidGroup;
}){
const [ displayCreatePersonModal, setDisplayCreatePersonModal ] = 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 personCountQuery = useGetPeopleByRaidGroupCount(raidGroup?.raidGroupId ?? "", sentSearchTerm);
const updateSearchTerm = useDebouncedCallback((newSearchTerm: string) => {
setSentSearchTerm(newSearchTerm);
}, 1000);
useEffect(() => {
updateSearchTerm(searchTerm ?? "");
}, [ searchTerm, updateSearchTerm ]);
useEffect(() => {
if(personCountQuery.status === "success"){
setTotalPages(Math.ceil(personCountQuery.data / pageSize));
}
}, [ personCountQuery ]);
return (
<>
<div
className="flex flex-row items-center justify-center w-full"
>
<div
className="flex flex-row items-center justify-center w-full"
>
&nbsp;
</div>
{/* Add Person Button */}
<div
className="flex flex-row items-center justify-center w-full"
>
<PrimaryButton
className="mb-8 text-nowrap"
onClick={() => setDisplayCreatePersonModal(true)}
>
Create Person
</PrimaryButton>
<PersonModal
display={displayCreatePersonModal}
close={() => setDisplayCreatePersonModal(false)}
person={undefined}
raidGroupId={raidGroup.raidGroupId ?? ""}
/>
</div>
{/* Person Search Box */}
<div
className="flex flex-row items-center justify-end w-full"
>
<div>
<TextInput
id={`personSearchBox${modalId}`}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search"
/>
</div>
</div>
</div>
{ /* Person List */}
<PersonLoader
raidGroup={raidGroup}
page={page}
pageSize={pageSize}
searchTerm={sentSearchTerm}
/>
{/* Pagination */}
<div
className="my-12"
>
<Pagination
currentPage={page}
totalPages={totalPages}
onChange={setPage}
/>
</div>
</>
);
}

View File

@@ -0,0 +1,65 @@
import DangerButton from "@/components/button/DangerButton";
import SecondaryButton from "@/components/button/SecondaryButton";
import RaidBuilderModal from "@/components/modal/RaidBuilderModal";
import { useDeletePerson } from "@/hooks/PersonHooks";
import { Person } from "@/interface/Person";
import { useTimedModal } from "@/providers/TimedModalProvider";
import { useEffect } from "react";
export default function DeletePersonModal({
display,
close,
raidGroupId,
person
}:{
display: boolean;
close: () => void;
raidGroupId: string;
person?: Person;
}){
const deletePersonMutate = useDeletePerson();
const { addSuccessMessage, addErrorMessage } = useTimedModal();
const deletePerson = () => {
deletePersonMutate.mutate({raidGroupId, personId: person?.personId ?? ""});
}
useEffect(() => {
if(deletePersonMutate.status === "success"){
deletePersonMutate.reset();
addSuccessMessage(`Person ${person?.personName} deleted`);
close();
}
else if(deletePersonMutate.status === "error"){
deletePersonMutate.reset();
addErrorMessage(`Error deleting person ${person?.personName}: ${deletePersonMutate.error.message}`);
console.log(deletePersonMutate.error);
}
})
return (
<RaidBuilderModal
display={display}
close={close}
modalHeader={`Delete Person ${person?.personName}`}
modalBody={`Are you sure you want to delete ${person?.personName}`}
modalFooter={
<>
<DangerButton
onClick={deletePerson}
>
Delete
</DangerButton>
<SecondaryButton
onClick={close}
>
Cancel
</SecondaryButton>
</>
}
/>
);
}

View File

@@ -0,0 +1,110 @@
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 { useCreatePerson, useUpdatePerson } from "@/hooks/PersonHooks";
import { Person } from "@/interface/Person";
import { useTimedModal } from "@/providers/TimedModalProvider";
import { useEffect, useState } from "react";
export default function PersonModal({
display,
close,
person,
raidGroupId
}:{
display: boolean;
close: () => void;
person: Person | undefined;
raidGroupId: string;
}){
const [ personName, setPersonName ] = useState<string>(person?.personName ?? "");
const [ discordId, setDiscordId ] = useState<string>(person?.discordId ?? "");
const modalId = crypto.randomUUID().replaceAll("-", "");
useEffect(() => {
setPersonName(person?.personName ?? "");
setDiscordId(person?.discordId ?? "");
}, [ person, raidGroupId, setPersonName, setDiscordId ]);
const createPersonMutate = useCreatePerson();
const updatePersonMutate = useUpdatePerson();
const { addSuccessMessage, addErrorMessage } = useTimedModal();
useEffect(() => {
if(createPersonMutate.status === "success"){
createPersonMutate.reset();
addSuccessMessage("Person created successfully");
close();
}
else if(updatePersonMutate.status === "success"){
updatePersonMutate.reset();
addSuccessMessage("Person updated successfully");
close();
}
else if(createPersonMutate.status === "error"){
createPersonMutate.reset();
addErrorMessage(`Error creating person ${personName}: ${createPersonMutate.error.message}`);
console.log(createPersonMutate.error);
}
else if(updatePersonMutate.status === "error"){
updatePersonMutate.reset();
addErrorMessage(`Error updating person ${personName}: ${updatePersonMutate.error.message}`);
console.log(updatePersonMutate.error);
}
});
const updatePerson = () => {
updatePersonMutate.mutate({raidGroupId, personId: person?.personId, personName, discordId} as Person);
}
const createPerson = () => {
createPersonMutate.mutate({raidGroupId, personName, discordId} as Person);
}
return (
<RaidBuilderModal
display={display}
close={close}
modalHeader={person ? "Update Person" : "Create Person"}
modalBody={
<div
className="flex flex-col items-center justify-center gap-4"
>
<TextInput
id={`personModalName${modalId}`}
placeholder="Person Name"
value={personName}
onChange={(e) => setPersonName(e.target.value)}
/>
<TextInput
id={`personModalDiscordId${modalId}`}
placeholder="Discord ID"
value={discordId}
onChange={(e) => setDiscordId(e.target.value)}
/>
</div>
}
modalFooter={
<>
<PrimaryButton
onClick={person ? updatePerson : createPerson}
>
{person ? "Update" : "Create"}
</PrimaryButton>
<SecondaryButton
onClick={close}
>
Cancel
</SecondaryButton>
</>
}
/>
);
}

View File

@@ -0,0 +1,131 @@
import PrimaryButton from "@/components/button/PrimaryButton";
import SecondaryButton from "@/components/button/SecondaryButton";
import { GameClassSelector } from "@/components/gameClass/GameClassSelector";
import TextInput from "@/components/input/TextInput";
import RaidBuilderModal from "@/components/modal/RaidBuilderModal";
import RatingSelector from "@/components/personCharacter/RatingSelector";
import { useCreatePersonCharacter, useUpdatePersonCharacter } from "@/hooks/PersonCharacterHooks";
import { PersonCharacter } from "@/interface/PersonCharacter";
import { useTimedModal } from "@/providers/TimedModalProvider";
import { useEffect, useState } from "react";
export default function PersonCharacterModal({
display,
close,
personCharacter,
personId,
gameId,
raidGroupId
}:{
display: boolean;
close: () => void;
personCharacter?: PersonCharacter;
personId: string;
gameId: string;
raidGroupId: string;
}){
const [ characterName, setCharacterName ] = useState<string>(personCharacter?.characterName ?? "");
const [ gameClassId, setGameClassId ] = useState<string | undefined>(personCharacter?.gameClassId);
const [ characterRating, setCharacterRating ] = useState<number>();
const [ characterComments, setCharacterComments ] = useState<string>();
const modalId = crypto.randomUUID().replaceAll("-", "");
useEffect(() => {
if(personCharacter){
setCharacterName(personCharacter.characterName);
setGameClassId(personCharacter.gameClassId);
setCharacterRating(personCharacter.characterRating);
setCharacterComments(personCharacter.characterComments);
}
}, [ personCharacter, setCharacterName, setGameClassId, setCharacterRating, setCharacterComments ]);
const createPersonCharacterMutate = useCreatePersonCharacter(raidGroupId, personId);
const updatePersonCharacterMutate = useUpdatePersonCharacter(raidGroupId, personId);
const { addSuccessMessage, addErrorMessage } = useTimedModal();
useEffect(() => {
if(createPersonCharacterMutate.status === "success"){
createPersonCharacterMutate.reset();
addSuccessMessage(`Character ${characterName} created successfully`);
close();
}
else if(updatePersonCharacterMutate.status === "success"){
updatePersonCharacterMutate.reset();
addSuccessMessage(`Character ${characterName} updated successfully`);
close();
}
else if(createPersonCharacterMutate.status === "error"){
createPersonCharacterMutate.reset();
addErrorMessage(`Error creating character ${characterName}: ${createPersonCharacterMutate.error.message}`);
console.log(createPersonCharacterMutate.error);
}
else if(updatePersonCharacterMutate.status === "error"){
updatePersonCharacterMutate.reset();
addErrorMessage(`Error updating character ${characterName}: ${updatePersonCharacterMutate.error.message}`);
console.log(updatePersonCharacterMutate.error);
}
});
const createPersonCharacter = () => {
createPersonCharacterMutate.mutate({personId, characterName, gameClassId, characterRating, characterComments} as PersonCharacter);
}
const updatePersonCharacter = () => {
updatePersonCharacterMutate.mutate({personCharacterId: personCharacter?.personCharacterId, personId, characterName, gameClassId, characterRating, characterComments} as PersonCharacter);
}
return (
<RaidBuilderModal
display={display}
close={close}
modalHeader={personCharacter ? "Update Character" : "Create Character"}
modalBody={
<div
className="flex flex-col items-center justify-center gap-4"
>
<TextInput
id={`personCharacterModalCharacterName${modalId}`}
placeholder="Character Name"
value={characterName}
onChange={(e) => setCharacterName(e.target.value)}
/>
<GameClassSelector
gameId={gameId}
gameClassId={personCharacter?.gameClassId}
onChange={setGameClassId}
/>
<RatingSelector
rating={characterRating}
onChange={setCharacterRating}
/>
<TextInput
id={`personCharacterModalCharacterComments${modalId}`}
placeholder="Character Comments"
value={characterComments}
onChange={(e) => setCharacterComments(e.target.value)}
/>
</div>
}
modalFooter={
<>
<PrimaryButton
onClick={personCharacter ? updatePersonCharacter : createPersonCharacter}
>
{personCharacter ? "Update" : "Create"}
</PrimaryButton>
<SecondaryButton
onClick={close}
>
Cancel
</SecondaryButton>
</>
}
/>
);
}

View File

@@ -47,10 +47,8 @@ export default function RaidGroupsList({
className="absolute -my-4" className="absolute -my-4"
> >
<img <img
className="m-auto" className="m-auto max-h-14 max-w-14"
src={`${import.meta.env.VITE_ICON_URL}/raidGroupIcons/${raidGroup.raidGroupIcon}`} src={`${import.meta.env.VITE_ICON_URL}/raidGroup/${raidGroup.raidGroupIcon}`}
height={56}
width={56}
/> />
</div> </div>
} }