From c9ceeea3b4a43a49575364f70975ca284c17cd7d Mon Sep 17 00:00:00 2001 From: Mattrixwv Date: Mon, 10 Mar 2025 22:55:16 -0400 Subject: [PATCH] Person page working --- .../gameClass/GameClassCellDisplay.tsx | 32 +++++ .../personCharacter/RatingSelector.tsx | 35 ++--- src/components/table/TableBody.tsx | 2 +- src/hooks/GameClassHooks.ts | 19 +++ src/hooks/PersonCharacterHooks.ts | 52 ++++++++ src/pages/protected/PersonPage.tsx | 74 ++++++++++- src/ui/account/AdminAccountsTab.tsx | 4 +- src/ui/classGroup/ClassGroupsTab.tsx | 4 +- src/ui/person/PersonAdminButtons.tsx | 25 ++-- src/ui/person/PersonDisplay.tsx | 122 +++++++++++++++++ src/ui/person/PersonHeader.tsx | 57 ++++++++ src/ui/person/PersonList.tsx | 13 +- src/ui/person/PersonListSkeleton.tsx | 1 + src/ui/person/PersonTab.tsx | 4 +- src/ui/person/modals/DeletePersonModal.tsx | 2 +- .../PersonCharacterAdminButtons.tsx | 38 ++++++ .../personCharacter/PersonCharacterList.tsx | 125 ++++++++++++++++++ .../PersonCharacterListSkeleton.tsx | 84 ++++++++++++ .../personCharacter/PersonCharacterLoader.tsx | 40 ++++++ .../modal/DeletePersonCharacterModal.tsx | 67 ++++++++++ src/ui/raidLayout/RaidLayoutTab.tsx | 4 +- 21 files changed, 757 insertions(+), 47 deletions(-) create mode 100644 src/components/gameClass/GameClassCellDisplay.tsx create mode 100644 src/ui/person/PersonDisplay.tsx create mode 100644 src/ui/person/PersonHeader.tsx create mode 100644 src/ui/personCharacter/PersonCharacterAdminButtons.tsx create mode 100644 src/ui/personCharacter/PersonCharacterList.tsx create mode 100644 src/ui/personCharacter/PersonCharacterListSkeleton.tsx create mode 100644 src/ui/personCharacter/PersonCharacterLoader.tsx create mode 100644 src/ui/personCharacter/modal/DeletePersonCharacterModal.tsx diff --git a/src/components/gameClass/GameClassCellDisplay.tsx b/src/components/gameClass/GameClassCellDisplay.tsx new file mode 100644 index 0000000..1729adc --- /dev/null +++ b/src/components/gameClass/GameClassCellDisplay.tsx @@ -0,0 +1,32 @@ +import { useGetGameClass } from "@/hooks/GameClassHooks"; +import DangerMessage from "../message/DangerMessage"; + + +export default function GameClassCellDisplay({ + gameClassId +}:{ + gameClassId: string; +}){ + const gameClassQuery = useGetGameClass(gameClassId); + + + if(gameClassQuery.status === "pending"){ + return
Loading...
; + } + else if(gameClassQuery.status === "error"){ + return Error: {gameClassQuery.error.message} + } + else{ + return ( +
+ + {gameClassQuery.data.gameClassName} +
+ ); + } +} diff --git a/src/components/personCharacter/RatingSelector.tsx b/src/components/personCharacter/RatingSelector.tsx index bc2cab8..fd8d8a4 100644 --- a/src/components/personCharacter/RatingSelector.tsx +++ b/src/components/personCharacter/RatingSelector.tsx @@ -22,22 +22,25 @@ export default function RatingSelector({ return (
- +
); } diff --git a/src/components/table/TableBody.tsx b/src/components/table/TableBody.tsx index 8b8b79e..86a1e42 100644 --- a/src/components/table/TableBody.tsx +++ b/src/components/table/TableBody.tsx @@ -25,7 +25,7 @@ export default function TableBody({ >
{ + const response = await api.get(`/gameClass/${gameClassId}`); + + if(response.status !== 200){ + throw new Error("Failed to get game class"); + } + else if(response.data.errors){ + throw new Error(response.data.errors.join(", ")); + } + + return response.data as GameClass; + }, + enabled: !!gameClassId + }) +} + export function useGetGameClasses(gameId: string, page: number, pageSize: number, searchTerm?: string){ return useQuery({ queryKey: ["gameClasses", gameId, { page, pageSize, searchTerm }], diff --git a/src/hooks/PersonCharacterHooks.ts b/src/hooks/PersonCharacterHooks.ts index 76f5097..d4b0990 100644 --- a/src/hooks/PersonCharacterHooks.ts +++ b/src/hooks/PersonCharacterHooks.ts @@ -21,6 +21,58 @@ export function useGetPersonCharactersByPersonId(personId: string, raidGroupId: }); } +export function useGetPersonCharactersByPersonIdSearch(personId: string, raidGroupId: string, page: number, pageSize: number, searchTerm?: string){ + const params = new URLSearchParams(); + params.append("page", page.toString()); + params.append("pageSize", pageSize.toString()); + if(searchTerm){ + params.append("searchTerm", searchTerm); + } + + + return useQuery({ + queryKey: ["personCharacters", personId, { page, pageSize, searchTerm }], + queryFn: async () => { + const response = await api.get(`/raidGroup/${raidGroupId}/person/${personId}/character/page?${params}`); + + if(response.status !== 200){ + throw new Error("Failed to get person characters"); + } + else if(response.data.errors){ + throw new Error(response.data.errors.join(", ")); + } + + console.log("person characters"); + console.log(response.data); + + return response.data as PersonCharacter[]; + } + }); +} + +export function useGetPersonCharactersCountByPersonIdSearch(personId: string, raidGroupId: string, searchTerm?: string){ + const params = new URLSearchParams(); + if(searchTerm){ + params.append("searchTerm", searchTerm); + } + + return useQuery({ + queryKey: ["personCharactersCount", personId, { searchTerm }], + queryFn: async () => { + const response = await api.get(`/raidGroup/${raidGroupId}/person/${personId}/character/count?${params}`); + + if(response.status !== 200){ + throw new Error("Failed to get person characters count"); + } + else if(response.data.errors){ + throw new Error(response.data.errors.join(", ")); + } + + return response.data.count as number; + } + }); +} + export function useCreatePersonCharacter(raidGroupId: string, personId: string){ const queryClient = useQueryClient(); diff --git a/src/pages/protected/PersonPage.tsx b/src/pages/protected/PersonPage.tsx index 6a65013..8e25ca5 100644 --- a/src/pages/protected/PersonPage.tsx +++ b/src/pages/protected/PersonPage.tsx @@ -1,10 +1,72 @@ +import DangerMessage from "@/components/message/DangerMessage"; +import { useGetPerson } from "@/hooks/PersonHooks"; +import { useGetRaidGroup } from "@/hooks/RaidGroupHooks"; +import { Person } from "@/interface/Person"; +import { RaidGroup } from "@/interface/RaidGroup"; +import PersonDisplay from "@/ui/person/PersonDisplay"; +import { PersonHeader } from "@/ui/person/PersonHeader"; +import { useEffect, useState } from "react"; +import { useParams } from "react-router"; + + export default function PersonPage(){ - //TODO: + const { raidGroupId, personId } = useParams(); + const [ raidGroup, setRaidGroup ] = useState(); + const [ person, setPerson ] = useState(); - return ( -
- Person Page -
- ); + const raidGroupQuery = useGetRaidGroup(raidGroupId ?? "", false); + const personQuery = useGetPerson(raidGroupId ?? "", personId ?? "", false); + useEffect(() => { + if(raidGroupQuery.status === "success"){ + setRaidGroup(raidGroupQuery.data); + } + }, [ raidGroupQuery ]); + useEffect(() => { + if(personQuery.status === "success"){ + setPerson(personQuery.data); + } + }, [ personQuery ]); + + if(raidGroupQuery.status === "pending" || personQuery.status === "pending"){ + return ( +
Loading...
+ ); + } + else if(raidGroupQuery.status === "error"){ + return ( + Error: {raidGroupQuery.error.message} + ); + } + else if (personQuery.status === "error"){ + return ( + Error: {personQuery.error.message} + ); + } + else if(raidGroupQuery.status === "success" && raidGroupQuery.data === undefined){ + return ( + Raid Group not found + ); + } + else if(personQuery.status === "success" && personQuery.data === undefined){ + return ( + Person not found + ); + } + else if(raidGroup && person){ + return ( +
+ + +
+ ); + } } diff --git a/src/ui/account/AdminAccountsTab.tsx b/src/ui/account/AdminAccountsTab.tsx index 5680d22..fbcb7e1 100644 --- a/src/ui/account/AdminAccountsTab.tsx +++ b/src/ui/account/AdminAccountsTab.tsx @@ -41,7 +41,7 @@ export default function AdminAccountsTab(){ return ( <>
setDisplayCreateAccountModal(true)} > Create Account diff --git a/src/ui/classGroup/ClassGroupsTab.tsx b/src/ui/classGroup/ClassGroupsTab.tsx index 3c84d33..03c4ffb 100644 --- a/src/ui/classGroup/ClassGroupsTab.tsx +++ b/src/ui/classGroup/ClassGroupsTab.tsx @@ -46,7 +46,7 @@ export default function ClassGroupsTab({ return ( <>
setDisplayCreateClassGroupModal(true)} > Create Class Group diff --git a/src/ui/person/PersonAdminButtons.tsx b/src/ui/person/PersonAdminButtons.tsx index 3580ae1..c8b603c 100644 --- a/src/ui/person/PersonAdminButtons.tsx +++ b/src/ui/person/PersonAdminButtons.tsx @@ -14,22 +14,25 @@ export default function PersonAdminButtons({ buttonProps: ButtonProps; showUpdatePersonModal: () => void; showDeletePersonModal: () => void; - showCreatePersonCharacterModal: () => void; + showCreatePersonCharacterModal?: () => void; }){ return (
- - - + { + showCreatePersonCharacterModal && + + + + } (); + const pageSize = 10; + const modalId = crypto.randomUUID().replaceAll("-", ""); + + const navigate = useNavigate(); + + + const characterCountQuery = useGetPersonCharactersCountByPersonIdSearch(person.personId ?? "", raidGroup.raidGroupId ?? "", sentSearchTerm); + + + const updateSearchTerm = useDebouncedCallback((newSearchTerm: string) => { + setSentSearchTerm(newSearchTerm); + }, 1000); + + + useEffect(() => { + updateSearchTerm(searchTerm ?? ""); + }, [ searchTerm, updateSearchTerm ]); + + + useEffect(() => { + if(characterCountQuery.status === "success"){ + setTotalPages(Math.ceil(characterCountQuery.data / pageSize)); + } + }, [ characterCountQuery ]); + + + return ( + <> +
+
+ navigate(`/raidGroup/${raidGroup.raidGroupId}`)} + > + + +
+ {/* Add Character Button */} +
+ setDisplayCreatePersonCharacterModal(true)} + > + Create Character + + setDisplayCreatePersonCharacterModal(false)} + personId={person.personId ?? ""} + raidGroupId={raidGroup.raidGroupId ?? ""} + gameId={raidGroup.gameId} + /> +
+ {/* Character Search Box */} +
+
+ setSearchTerm(e.target.value)} + placeholder="Search" + /> +
+
+
+ {/* Character List */} + + {/* Pagination */} +
+ +
+ + ); +} diff --git a/src/ui/person/PersonHeader.tsx b/src/ui/person/PersonHeader.tsx new file mode 100644 index 0000000..5d4de7e --- /dev/null +++ b/src/ui/person/PersonHeader.tsx @@ -0,0 +1,57 @@ +import { ButtonProps } from "@/components/button/Button"; +import { Person } from "@/interface/Person"; +import { RaidGroup } from "@/interface/RaidGroup"; +import { useState } from "react"; +import PersonAdminButtons from "./PersonAdminButtons"; +import DeletePersonModal from "./modals/DeletePersonModal"; +import PersonModal from "./modals/PersonModal"; + + +export function PersonHeader({ + raidGroup, + person +}:{ + raidGroup: RaidGroup; + person: Person; +}){ + const [ displayEditPersonModal, setDisplayEditPersonModal ] = useState(false); + const [ displayDeletePersonModal, setDisplayDeletePersonModal ] = useState(false); + + const buttonProps: ButtonProps = { + variant: "ghost", + size: "md", + shape: "square" + }; + + + return ( +

+
+ {person.personName} +
+
+ setDisplayEditPersonModal(true)} + showDeletePersonModal={() => setDisplayDeletePersonModal(true)} + /> +
+ setDisplayEditPersonModal(false)} + person={person} + raidGroupId={raidGroup.raidGroupId ?? ""} + /> + setDisplayDeletePersonModal(false)} + person={person} + raidGroupId={raidGroup.raidGroupId ?? ""} + /> +

+ ); +} diff --git a/src/ui/person/PersonList.tsx b/src/ui/person/PersonList.tsx index a47cf5a..3100553 100644 --- a/src/ui/person/PersonList.tsx +++ b/src/ui/person/PersonList.tsx @@ -4,6 +4,7 @@ import Table from "@/components/table/Table"; import { Person } from "@/interface/Person"; import { RaidGroup } from "@/interface/RaidGroup"; import { useState } from "react"; +import { Link } from "react-router"; import PersonCharacterModal from "../personCharacter/modal/PersonCharacterModal"; import DeletePersonModal from "./modals/DeletePersonModal"; import PersonModal from "./modals/PersonModal"; @@ -52,18 +53,22 @@ export default function PersonList({ ]; const bodyElements: React.ReactNode[][] = people.map((person) => [ -
+ {person.personName} -
, + ,
{person.discordId}
, -
+ -
, + ,
diff --git a/src/ui/person/PersonListSkeleton.tsx b/src/ui/person/PersonListSkeleton.tsx index bf069b3..bcc46a0 100644 --- a/src/ui/person/PersonListSkeleton.tsx +++ b/src/ui/person/PersonListSkeleton.tsx @@ -4,6 +4,7 @@ import { elementBg } from "@/util/SkeletonUtil"; import React from "react"; import PersonAdminButtons from "./PersonAdminButtons"; + export default function PersonListSkeleton(){ const headElements: React.ReactNode[] = [
diff --git a/src/ui/person/PersonTab.tsx b/src/ui/person/PersonTab.tsx index f472c47..4fa5265 100644 --- a/src/ui/person/PersonTab.tsx +++ b/src/ui/person/PersonTab.tsx @@ -46,7 +46,7 @@ export default function PersonTab({ return ( <>
setDisplayCreatePersonModal(true)} > Create Person diff --git a/src/ui/person/modals/DeletePersonModal.tsx b/src/ui/person/modals/DeletePersonModal.tsx index 76f90ef..1ce071c 100644 --- a/src/ui/person/modals/DeletePersonModal.tsx +++ b/src/ui/person/modals/DeletePersonModal.tsx @@ -37,7 +37,7 @@ export default function DeletePersonModal({ addErrorMessage(`Error deleting person ${person?.personName}: ${deletePersonMutate.error.message}`); console.log(deletePersonMutate.error); } - }) + }); return ( diff --git a/src/ui/personCharacter/PersonCharacterAdminButtons.tsx b/src/ui/personCharacter/PersonCharacterAdminButtons.tsx new file mode 100644 index 0000000..3f40509 --- /dev/null +++ b/src/ui/personCharacter/PersonCharacterAdminButtons.tsx @@ -0,0 +1,38 @@ +import { ButtonProps } from "@/components/button/Button"; +import DangerButton from "@/components/button/DangerButton"; +import PrimaryButton from "@/components/button/PrimaryButton"; +import { BsPencilFill, BsTrash3 } from "react-icons/bs"; + + +export default function PersonCharacterAdminButtons({ + buttonProps, + showUpdatePersonCharacterModal, + showDeletePersonCharacterModal +}:{ + buttonProps: ButtonProps; + showUpdatePersonCharacterModal: () => void; + showDeletePersonCharacterModal: () => void; +}){ + return ( +
+ + + + + + +
+ ); +} diff --git a/src/ui/personCharacter/PersonCharacterList.tsx b/src/ui/personCharacter/PersonCharacterList.tsx new file mode 100644 index 0000000..9ffbcac --- /dev/null +++ b/src/ui/personCharacter/PersonCharacterList.tsx @@ -0,0 +1,125 @@ +import { ButtonProps } from "@/components/button/Button"; +import GameClassCellDisplay from "@/components/gameClass/GameClassCellDisplay"; +import Table from "@/components/table/Table"; +import { Person } from "@/interface/Person"; +import { PersonCharacter } from "@/interface/PersonCharacter"; +import { RaidGroup } from "@/interface/RaidGroup"; +import { useState } from "react"; +import DeletePersonCharacterModal from "./modal/DeletePersonCharacterModal"; +import PersonCharacterModal from "./modal/PersonCharacterModal"; +import PersonCharacterAdminButtons from "./PersonCharacterAdminButtons"; + + +export default function PersonCharacterList({ + personCharacters, + person, + raidGroup +}:{ + personCharacters: PersonCharacter[]; + person: Person; + raidGroup: RaidGroup; +}){ + const [ selectedCharacter, setSelectedCharacter ] = useState(); + const [ displayUpdatePersonCharacterModal, setDisplayUpdatePersonCharacterModal ] = useState(false); + const [ displayDeletePersonCharacterModal, setDisplayDeletePersonCharacterModal ] = useState(false); + + + const buttonProps: ButtonProps = { + variant: "ghost", + size: "md", + shape: "square" + }; + + + const headElements: React.ReactNode[] = [ +
+ Character Name +
, +
+ Class +
, +
+ Rating +
, +
+ Comments +
, +
+ Actions +
+ ]; + + const bodyElements: React.ReactNode[][] = personCharacters.map((personCharacter) => [ +
+ {personCharacter.characterName} +
, +
+ +
, +
+ { + personCharacter.characterRating && personCharacter.characterRating > 0 ? + personCharacter.characterRating : + <>  + } +
, +
+ { + personCharacter.characterComments || <>  + } +
, +
+
+   +
+ { + setSelectedCharacter(personCharacter); + setDisplayUpdatePersonCharacterModal(true); + }} + showDeletePersonCharacterModal={() => { + setSelectedCharacter(personCharacter); + setDisplayDeletePersonCharacterModal(true); + }} + /> +
+ ]); + + + return ( + <> + + {setDisplayUpdatePersonCharacterModal(false); setSelectedCharacter(undefined);}} + personCharacter={selectedCharacter} + personId={person.personId ?? ""} + gameId={raidGroup.gameId} + raidGroupId={raidGroup.raidGroupId ?? ""} + /> + {setDisplayDeletePersonCharacterModal(false); setSelectedCharacter(undefined);}} + personCharacter={selectedCharacter} + raidGroupId={raidGroup.raidGroupId ?? ""} + personId={person.personId ?? ""} + /> + + ); +} diff --git a/src/ui/personCharacter/PersonCharacterListSkeleton.tsx b/src/ui/personCharacter/PersonCharacterListSkeleton.tsx new file mode 100644 index 0000000..da1abe1 --- /dev/null +++ b/src/ui/personCharacter/PersonCharacterListSkeleton.tsx @@ -0,0 +1,84 @@ +import { ButtonShape, ButtonSizeType, ButtonVariant } from "@/components/button/Button"; +import Table from "@/components/table/Table"; +import { elementBg } from "@/util/SkeletonUtil"; +import PersonCharacterAdminButtons from "./PersonCharacterAdminButtons"; + + +export default function PersonCharacterListSkeleton(){ + const headElements: React.ReactNode[] = [ +
+ Character Name +
, +
+ Class +
, +
+ Rating +
, +
+ Comments +
, +
+ Actions +
+ ]; + + const bodyElements: React.ReactNode[][] = [ + PersonCharacterSkeleton(), + PersonCharacterSkeleton(), + PersonCharacterSkeleton(), + PersonCharacterSkeleton(), + PersonCharacterSkeleton(), + PersonCharacterSkeleton(), + PersonCharacterSkeleton(), + PersonCharacterSkeleton(), + PersonCharacterSkeleton(), + PersonCharacterSkeleton() + ]; + + + return ( +
+ ); +} + +function PersonCharacterSkeleton(): React.ReactNode[]{ + const buttonsProps = { + buttonProps: { + variant: "ghost" as ButtonVariant, + size: "md" as ButtonSizeType, + shape: "square" as ButtonShape, + disabled: true + }, + showUpdatePersonCharacterModal: () => {}, + showDeletePersonCharacterModal: () => {} + }; + + const elements: React.ReactNode[] = [ +
, +
, +
, +
, +
+
 
+ +
+ ]; + + return elements; +} diff --git a/src/ui/personCharacter/PersonCharacterLoader.tsx b/src/ui/personCharacter/PersonCharacterLoader.tsx new file mode 100644 index 0000000..36bb5cd --- /dev/null +++ b/src/ui/personCharacter/PersonCharacterLoader.tsx @@ -0,0 +1,40 @@ +import DangerMessage from "@/components/message/DangerMessage"; +import { useGetPersonCharactersByPersonIdSearch } from "@/hooks/PersonCharacterHooks"; +import { Person } from "@/interface/Person"; +import { RaidGroup } from "@/interface/RaidGroup"; +import PersonCharacterList from "./PersonCharacterList"; +import PersonCharacterListSkeleton from "./PersonCharacterListSkeleton"; + + +export default function PersonCharacterLoader({ + raidGroup, + person, + page, + pageSize, + searchTerm +}:{ + raidGroup: RaidGroup; + person: Person; + page: number; + pageSize: number; + searchTerm?: string; +}){ + const personCharactersQuery = useGetPersonCharactersByPersonIdSearch(person.personId ?? "", raidGroup.raidGroupId ?? "", page - 1, pageSize, searchTerm); + + + if(personCharactersQuery.status === "pending"){ + + } + else if(personCharactersQuery.status === "error"){ + return Error: {personCharactersQuery.error.message} + } + else{ + return ( + + ); + } +} diff --git a/src/ui/personCharacter/modal/DeletePersonCharacterModal.tsx b/src/ui/personCharacter/modal/DeletePersonCharacterModal.tsx new file mode 100644 index 0000000..48b3869 --- /dev/null +++ b/src/ui/personCharacter/modal/DeletePersonCharacterModal.tsx @@ -0,0 +1,67 @@ +import DangerButton from "@/components/button/DangerButton"; +import SecondaryButton from "@/components/button/SecondaryButton"; +import RaidBuilderModal from "@/components/modal/RaidBuilderModal"; +import { useDeletePersonCharacter } from "@/hooks/PersonCharacterHooks"; +import { PersonCharacter } from "@/interface/PersonCharacter"; +import { useTimedModal } from "@/providers/TimedModalProvider"; +import { useEffect } from "react"; + + +export default function DeletePersonCharacterModal({ + display, + close, + personCharacter, + raidGroupId, + personId +}:{ + display: boolean; + close: () => void; + personCharacter?: PersonCharacter; + raidGroupId: string; + personId: string; +}){ + const deletePersonCharacterMutate = useDeletePersonCharacter(raidGroupId, personId); + const { addSuccessMessage, addErrorMessage } = useTimedModal(); + + + const deletePersonCharacter = () => { + deletePersonCharacterMutate.mutate(personCharacter?.personCharacterId ?? ""); + } + + useEffect(() => { + if(deletePersonCharacterMutate.status === "success"){ + deletePersonCharacterMutate.reset(); + addSuccessMessage("Person Character Deleted"); + close(); + } + else if(deletePersonCharacterMutate.status === "error"){ + deletePersonCharacterMutate.reset(); + addErrorMessage("Failed to delete person character"); + console.log(deletePersonCharacterMutate.error); + } + }); + + + return ( + + + Delete + + + Cancel + + + } + /> + ); +} diff --git a/src/ui/raidLayout/RaidLayoutTab.tsx b/src/ui/raidLayout/RaidLayoutTab.tsx index e3dd00c..950a777 100644 --- a/src/ui/raidLayout/RaidLayoutTab.tsx +++ b/src/ui/raidLayout/RaidLayoutTab.tsx @@ -45,7 +45,7 @@ export default function RaidLayoutTab({ return ( <>
setDisplayCreateRaidLayoutModal(true)} > Create Raid Layout