Game calendar working

This commit is contained in:
2025-03-06 19:49:03 -05:00
parent ef6da3ea64
commit 28462776ac
30 changed files with 1025 additions and 67 deletions

View File

@@ -8,7 +8,7 @@ export default function AccountStatusSelector({
value: AccountStatus;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}){
const modalId = crypto.randomUUID().replace("-", "");
const modalId = crypto.randomUUID().replaceAll("-", "");
return (

View File

@@ -18,7 +18,7 @@ export default function GameSelector({
const [ searchTerm, setSearchTerm ] = useState(game?.gameName ?? "");
const [ searching, setSearching ] = useState(false);
const modalId = crypto.randomUUID().replace("-", "");
const modalId = crypto.randomUUID().replaceAll("-", "");
const gameSearchQuery = useGetGames(0, 5, gameSearch);

View File

@@ -0,0 +1,47 @@
import clsx from "clsx";
import { ComponentProps } from "react";
interface DateInputProps extends ComponentProps<"input">{
id: string;
inputClasses?: string;
labelClasses?: string;
accepted?: boolean;
}
export default function DateInput(props: DateInputProps){
const { id, placeholder, inputClasses, labelClasses } = props;
return (
<div
className="bg-inherit px-4 pb-4 rounded-sm w-full md:flex md:justify-center"
>
<div
className="relative bg-inherit w-full"
>
<input
{...props}
id={id}
className={clsx(
"peer px-2 py-1 rounded-lg ring-2 ring-gray-500 focus:ring-sky-600 outline-hidden w-full",
inputClasses
)}
type="datetime-local"
/>
<label
htmlFor={id}
id={`${id}Label`}
className={clsx(
"absolute cursor-pointer left-0 -top-3 mx-1 px-1",
"bg-white dark:bg-neutral-825 text-sm peer-focus:text-sky-600",
labelClasses
)}
>
{placeholder}
</label>
</div>
</div>
);
}

View File

@@ -0,0 +1,64 @@
import clsx from "clsx";
import { ComponentProps } from "react";
export interface TextAreaProps extends ComponentProps<"textarea">{
id: string;
inputClasses?: string;
labelClasses?: string;
accepted?: boolean;
}
export default function TextArea(props: TextAreaProps){
const { id, placeholder, name, inputClasses, labelClasses, accepted } = props;
return (
<div
className="bg-inherit p-4 rounded-sm w-full md:flex md:justify-center"
>
<div
className="relative bg-inherit w-full"
>
<textarea
{...props}
id={id}
name={name}
className={clsx(
"peer bg-transparent w-full md:min-w-72 h-24 px-2 py-1 rounded-lg",
"ring-2 ring-gray-500 focus:ring-sky-600 placeholder-transparent outline-hidden",
inputClasses,
{
"ring-gray-500": accepted === undefined,
"ring-red-600": accepted === false,
"ring-green-600": accepted === true
}
)}
style={{resize: "none"}}
/>
<label
htmlFor={id}
id={`${id}Label`}
className={clsx(
"absolute cursor-text left-0 -top-3 mx-1 px-1",
"bg-white dark:bg-neutral-825 text-sm",
"peer-placeholder-shown:text-base peer-placeholder-shown:text-gray-500 peer-placeholder-shown:top-1 peer-focus:-top-3 peer-focus:text-sky-600 peer-focus:text-sm",
labelClasses,
{
"text-gray-500": accepted === undefined,
"text-red-600": accepted === false,
"text-green-600": accepted === true
}
)}
style={{
transitionProperty: "top, font-size, line-height",
transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
transitionDuration: "150ms"
}}
>
{placeholder}
</label>
</div>
</div>
);
}

View File

@@ -51,7 +51,8 @@ export default function TextInput(props: TextInputProps){
"text-green-600": accepted === true
}
)}
style={{transitionProperty: "top, font-size, line-height",
style={{
transitionProperty: "top, font-size, line-height",
transitionTimingFunction: "cubic-bezier(0.4, 0, 0.2, 1)",
transitionDuration: "150ms"
}}

View File

@@ -18,7 +18,7 @@ export default function ModalFooter(props: HTMLProps<HTMLDivElement>){
)}
>
<div
className="flex flex-row justify-center mx-8 my-3"
className="flex flex-row justify-center items-center w-full mx-8 my-3"
>
{children}
</div>

View File

@@ -49,7 +49,7 @@ export default function RaidBuilderModal({
className="bg-[#00000022] dark:bg-[#FFFFFF16]"
>
<div
className="flex flex-row items-center justify-center gap-4"
className="flex flex-row items-center justify-center gap-4 w-full"
>
{modalFooter}
</div>

167
src/hooks/CalendarHooks.ts Normal file
View File

@@ -0,0 +1,167 @@
import { CalendarEvent } from "@/interface/Calendar";
import { api } from "@/util/AxiosUtil";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export function useGetGameCalendar(gameId: string){
return useQuery({
queryKey: ["gameCalendar", gameId],
queryFn: async () => {
const response = await api.get(`/calendar/game/${gameId}`);
if(response.status !== 200){
throw new Error("Failed to get game calendar");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
return response.data as CalendarEvent[];
}
});
}
export function useGetRaidGroupCalendar(raidGroupId: string){
return useQuery({
queryKey: ["raidGroupCalendar", raidGroupId],
queryFn: async () => {
const response = await api.get(`/calendar/raidGroup/${raidGroupId}`);
if(response.status !== 200){
throw new Error("Failed to get raid group calendar");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
return response.data as CalendarEvent[];
}
});
}
export function useCreateGameCalendarEvent(gameId: string){
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (calendarEvent: CalendarEvent) => {
const response = await api.post(`/calendar/game/${gameId}`, {...calendarEvent, gameCalendarEventId: calendarEvent.calendarEventId, calendarEventId: undefined});
if(response.status !== 200){
throw new Error("Failed to create calendar event");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["gameCalendar"]})
}
});
}
export function useUpdateGameCalendarEvent(gameId: string){
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (calendarEvent: CalendarEvent) => {
const response = await api.put(`/calendar/game/${gameId}`, {...calendarEvent, gameCalendarEventId: calendarEvent.calendarEventId, calendarEventId: undefined});
if(response.status !== 200){
throw new Error("Failed to update calendar event");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["gameCalendar"]})
}
});
}
export function useDeleteGameCalendarEvent(gameId: string){
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (calendarEvent: CalendarEvent) => {
const response = await api.delete(`/calendar/game/${gameId}/${calendarEvent.calendarEventId}`);
if(response.status !== 200){
throw new Error("Failed to delete calendar event");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["gameCalendar"]})
}
});
}
export function useCreateRaidGroupCalendarEvent(raidGroupId: string){
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (calendarEvent: CalendarEvent) => {
const response = await api.post(`/calendar/raidGroup/${raidGroupId}`, {...calendarEvent, raidGroupCalendarEventId: calendarEvent.calendarEventId, calendarEventId: undefined});
if(response.status !== 200){
throw new Error("Failed to create calendar event");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["raidGroupCalendar"]})
}
});
}
export function useUpdateRaidGroupCalendarEvent(raidGroupId: string){
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (calendarEvent: CalendarEvent) => {
const response = await api.put(`/calendar/raidGroup/${raidGroupId}`, {...calendarEvent, raidGroupCalendarEventId: calendarEvent.calendarEventId, calendarEventId: undefined});
if(response.status !== 200){
throw new Error("Failed to update calendar event");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["raidGroupCalendar"]})
}
});
}
export function useDeleteRaidGroupCalendarEvent(raidGroupId: string){
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (calendarEvent: CalendarEvent) => {
const response = await api.delete(`/calendar/raidGroup/${raidGroupId}/${calendarEvent.calendarEventId}`);
if(response.status !== 200){
throw new Error("Failed to delete calendar event");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["raidGroupCalendar"]})
}
});
}

View File

@@ -5,7 +5,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
export function useGetGame(gameId: string, disabled: boolean){
return useQuery({
queryKey: ["game", gameId],
queryKey: ["games", gameId],
queryFn: async () => {
const response = await api.get(`/game/${gameId}`);
@@ -16,7 +16,7 @@ export function useGetGame(gameId: string, disabled: boolean){
throw new Error(response.data.errors.join(", "));
}
return response.data as Game;
return response.data?.gameId ? response.data as Game : undefined;
},
enabled: !disabled
});

View File

@@ -1,3 +1,4 @@
import { CalendarEvent } from "@/interface/Calendar";
import { RaidGroup } from "@/interface/RaidGroup";
import { api } from "@/util/AxiosUtil";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
@@ -52,6 +53,74 @@ export function useGetRaidGroupsCount(searchTerm?: string){
});
}
export function useGetRaidGroupsByGame(gameId: string, page: number, pageSize: number, searchTerm?: string){
return useQuery({
queryKey: ["raidGroups", gameId, { page, pageSize, searchTerm }],
queryFn: async () => {
const params = new URLSearchParams();
params.append("page", page.toString());
params.append("pageSize", pageSize.toString());
if(searchTerm){
params.append("searchTerm", searchTerm);
}
const response = await api.get(`/raidGroup/game/${gameId}?${params}`);
if(response.status !== 200){
throw new Error("Failed to get raid groups");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
return response.data as RaidGroup[];
}
});
}
export function useGetRaidGroupsByGameCount(gameId: string, searchTerm?: string){
const searchParams = new URLSearchParams();
if(searchTerm){
searchParams.append("searchTerm", searchTerm);
}
return useQuery({
queryKey: ["raidGroups", gameId, "count", searchTerm],
queryFn: async () => {
const response = await api.get(`/raidGroup/game/${gameId}/count?${searchParams}`);
if(response.status !== 200){
throw new Error("Failed to get raid groups count");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
return response.data.count as number;
}
});
}
export function useGetRaidGroupCalendar(raidGroupId: string){
return useQuery({
queryKey: ["raidGroups", raidGroupId, "calendar"],
queryFn: async () => {
const response = await api.get(`/raidGroup/${raidGroupId}/calendar`);
if(response.status !== 200){
throw new Error("Failed to get raid group calendar");
}
else if(response.data.errors){
throw new Error(response.data.errors.join(", "));
}
return response.data as CalendarEvent[];
}
});
}
export function useCreateRaidGroup(){
const queryClient = useQueryClient();

13
src/interface/Calendar.ts Normal file
View File

@@ -0,0 +1,13 @@
export interface CalendarEvent {
eventName: string;
eventDescription: string;
eventStartDate: Date;
eventEndDate: Date;
backgroundColor?: string;
calendarEventId?: string;
gameId?: string;
raidGroupId?: string;
raidInstanceId?: string;
}

View File

@@ -1,7 +1,7 @@
import TabGroup, { Tab } from "@/components/tab/TabGroup";
import AdminAccountsTab from "@/ui/account/AdminAccountsTab";
import AllGamesDisplay from "@/ui/game/AllGamesDisplay";
import AdminRaidGroupTab from "@/ui/raidGroup/AdminRaidGroupTab";
import AllRaidGroupsDisplay from "@/ui/raidGroup/AllRaidGroupsDisplay";
export default function AdminPage(){
@@ -16,7 +16,7 @@ export default function AdminPage(){
},
{
tabHeader: "Raid Groups",
tabContent: <AdminRaidGroupTab/>
tabContent: <AllRaidGroupsDisplay/>
}
];

View File

@@ -1,10 +1,66 @@
import TabGroup, { Tab } from "@/components/tab/TabGroup";
import { useGetGame } from "@/hooks/GameHooks";
import { Game } from "@/interface/Game";
import GameCalendarDisplay from "@/ui/calendar/GameCalendarDisplay";
import GameHeader from "@/ui/game/GameHeader";
import RaidGroupsByGameDisplay from "@/ui/raidGroup/RaidGroupsByGameDisplay";
import { useEffect, useState } from "react";
import { Navigate, useParams } from "react-router";
export default function GamePage(){
//TODO:
const { gameId } = useParams();
const tabs: Tab[] = [
{
tabHeader: "Calendar",
tabContent: <GameCalendarDisplay gameId={gameId!}/>
},
{
tabHeader: "Raid Groups",
tabContent: <RaidGroupsByGameDisplay gameId={gameId!}/>
},
{
tabHeader: "Classes",
tabContent: <div>Classes</div>
}
];
const [ game, setGame ] = useState<Game>();
const gameQuery = useGetGame(gameId ?? "", false);
useEffect(() => {
if(gameQuery.status === "success"){
setGame(gameQuery.data);
}
}, [ gameQuery ]);
return (
<div>
Game Page
</div>
);
if(gameQuery.status === "pending"){
return (
<div>Loading...</div>
);
}
else if(gameQuery.status === "error"){
return (
<div>Error</div>
);
}
else if(gameQuery.status === "success" && (gameQuery.data === undefined || gameQuery.data === undefined)){
return (
<Navigate to="/game"/>
);
}
else if(game){
return (
<main
className="flex flex-col items-center justify-center"
>
<GameHeader
game={game}
/>
<TabGroup
tabs={tabs}
/>
</main>
);
}
}

View File

@@ -15,7 +15,7 @@ export default function AdminAccountsTab(){
const [ searchTerm, setSearchTerm ] = useState<string>("");
const [ sentSearchTerm, setSentSearchTerm ] = useState<string>();
const pageSize = 10;
const modalId = crypto.randomUUID().replace("-", "");
const modalId = crypto.randomUUID().replaceAll("-", "");
const accountsCountQuery = useGetAccountsCount(sentSearchTerm);

View File

@@ -23,7 +23,7 @@ export default function AccountModal({
const [ email, setEmail ] = useState<string>(account?.email ?? "");
const [ password, setPassword ] = useState<string>("");
const [ accountStatus, setAccountStatus ] = useState<AccountStatus>(account?.accountStatus ?? AccountStatus.ACTIVE);
const modalId = crypto.randomUUID().replace("-", "");
const modalId = crypto.randomUUID().replaceAll("-", "");
useEffect(() => {

View File

@@ -22,7 +22,7 @@ export default function AccountPasswordRestModal({
const passwordResetMutate = useResetPassword(account?.accountId ?? "");
const modalId = crypto.randomUUID().replace("-", "");
const modalId = crypto.randomUUID().replaceAll("-", "");
const resetPassword = () => {

View File

@@ -0,0 +1,80 @@
import { useCreateGameCalendarEvent, useCreateRaidGroupCalendarEvent, useDeleteGameCalendarEvent, useDeleteRaidGroupCalendarEvent, useUpdateGameCalendarEvent, useUpdateRaidGroupCalendarEvent } from "@/hooks/CalendarHooks";
import { CalendarEvent } from "@/interface/Calendar";
import { calendarEventToFullCalendarEvent } from "@/util/CalendarUtil";
import { EventClickArg } from "@fullcalendar/core/index.js";
import dayGridPlugin from "@fullcalendar/daygrid";
import interactionPlugin, { DateClickArg } from "@fullcalendar/interaction";
import FullCalendar from "@fullcalendar/react";
import moment from "moment";
import { useState } from "react";
import CalendarEventModal from "./modals/CalendarEventModal";
export default function CalendarDisplay({
calendarEvents,
raidGroupId,
gameId
}:{
calendarEvents: CalendarEvent[];
raidGroupId?: string;
gameId?: string;
}){
const [ displayCalendarEventModal, setDisplayCalendarEventModal ] = useState(false);
const [ alterCalendarEvent, setAlterCalendarEvent ] = useState<CalendarEvent>();
const createGameCalendarEventMutate = useCreateGameCalendarEvent(gameId ?? "");
const updateGameCalendarEventMutate = useUpdateGameCalendarEvent(gameId ?? "");
const deleteGameCalendarEventMutate = useDeleteGameCalendarEvent(gameId ?? "");
const createRaidGroupCalendarEventMutate = useCreateRaidGroupCalendarEvent(raidGroupId ?? "");
const updateRaidGroupCalendarEventMutate = useUpdateRaidGroupCalendarEvent(raidGroupId ?? "");
const deleteRaidGroupCalendarEventMutate = useDeleteRaidGroupCalendarEvent(raidGroupId ?? "");
const newEvents = calendarEventToFullCalendarEvent(calendarEvents);
const showAddCalendarEventModal = (dateClickArg: DateClickArg) => {
console.log("showAdd()");
console.log(dateClickArg.date);
setAlterCalendarEvent({
eventStartDate: dateClickArg.date,
eventEndDate: moment(dateClickArg.date).add(1, "hours").toDate()
} as CalendarEvent);
setDisplayCalendarEventModal(true);
}
const showEditCalendarEventModal = (eventClickArg: EventClickArg) => {
setAlterCalendarEvent(calendarEvents.find((calEvent) => calEvent.calendarEventId === eventClickArg.event.id));
setDisplayCalendarEventModal(true);
}
const hideCalendarEventModal = () => {
setAlterCalendarEvent(undefined);
setDisplayCalendarEventModal(false);
}
return (
<div
className="w-full"
>
<FullCalendar
plugins={[ dayGridPlugin, interactionPlugin ]}
initialView="dayGridMonth"
events={newEvents}
eventClassNames="cursor-pointer my-2"
eventDisplay="block"
eventClick={showEditCalendarEventModal}
dateClick={showAddCalendarEventModal}
/>
<CalendarEventModal
display={displayCalendarEventModal}
close={hideCalendarEventModal}
calendarEvent={alterCalendarEvent}
createCalendarEventMutate={gameId ? createGameCalendarEventMutate : createRaidGroupCalendarEventMutate}
updateCalendarEventMutate={gameId ? updateGameCalendarEventMutate : updateRaidGroupCalendarEventMutate}
deleteCalendarEventMutate={gameId ? deleteGameCalendarEventMutate : deleteRaidGroupCalendarEventMutate}
/>
</div>
);
}

View File

@@ -0,0 +1,18 @@
import GameCalendarLoader from "./GameCalendarLoader";
export default function GameCalendarDisplay({
gameId
}:{
gameId: string;
}){
return (
<div
className="flex flex-col items-center justify-center"
>
<GameCalendarLoader
gameId={gameId}
/>
</div>
);
}

View File

@@ -0,0 +1,34 @@
import DangerMessage from "@/components/message/DangerMessage";
import { useGetGameCalendar } from "@/hooks/CalendarHooks";
import CalendarDisplay from "./CalendarDisplay";
export default function GameCalendarLoader({
gameId
}:{
gameId: string;
}){
const gameCalendarQuery = useGetGameCalendar(gameId);
if(gameCalendarQuery.status === "pending"){
return (
<div>
Loading...
</div>
);
}
else if(gameCalendarQuery.status === "error"){
return (
<DangerMessage>Error {gameCalendarQuery.error.message}</DangerMessage>
);
}
else{
return (
<CalendarDisplay
gameId={gameId}
calendarEvents={gameCalendarQuery.data}
/>
);
}
}

View File

@@ -0,0 +1,193 @@
import DangerButton from "@/components/button/DangerButton";
import PrimaryButton from "@/components/button/PrimaryButton";
import SecondaryButton from "@/components/button/SecondaryButton";
import DateInput from "@/components/input/DateInput";
import TextArea from "@/components/input/TextArea";
import TextInput from "@/components/input/TextInput";
import RaidBuilderModal from "@/components/modal/RaidBuilderModal";
import { CalendarEvent } from "@/interface/Calendar";
import { useTimedModal } from "@/providers/TimedModalProvider";
import { UseMutationResult } from "@tanstack/react-query";
import moment from "moment";
import { useEffect, useState } from "react";
import { BsTrash3 } from "react-icons/bs";
export default function CalendarEventModal({
display,
close,
calendarEvent,
createCalendarEventMutate,
updateCalendarEventMutate,
deleteCalendarEventMutate
}:{
display: boolean;
close: () => void;
calendarEvent?: CalendarEvent;
createCalendarEventMutate: UseMutationResult<void, Error, CalendarEvent, unknown>;
updateCalendarEventMutate: UseMutationResult<void, Error, CalendarEvent, unknown>;
deleteCalendarEventMutate: UseMutationResult<void, Error, CalendarEvent, unknown>;
}){
const [ eventName, setEventName ] = useState<string>(calendarEvent?.eventName ?? "");
const [ eventDescription, setEventDescription ] = useState<string>(calendarEvent?.eventDescription ?? "");
const [ eventStartDate, setEventStartDate ] = useState(calendarEvent?.eventStartDate ?? new Date());
const [ eventEndDate, setEventEndDate ] = useState(calendarEvent?.eventEndDate ?? new Date());
const { addSuccessMessage, addErrorMessage } = useTimedModal();
const modalId = crypto.randomUUID().replaceAll("-", "");
useEffect(() => {
if(createCalendarEventMutate.status === "success"){
createCalendarEventMutate.reset();
addSuccessMessage(`Calendar Event ${eventName} created successfully`);
close();
}
else if(updateCalendarEventMutate.status === "success"){
updateCalendarEventMutate.reset();
addSuccessMessage(`Calendar Event ${eventName} updated successfully`);
close();
}
else if(deleteCalendarEventMutate.status === "success"){
deleteCalendarEventMutate.reset();
addSuccessMessage(`Calendar Event ${eventName} deleted successfully`);
close();
}
else if(createCalendarEventMutate.status === "error"){
createCalendarEventMutate.reset();
addErrorMessage(`Error creating calendar event ${eventName}: ${createCalendarEventMutate.error.message}`);
console.log(createCalendarEventMutate.error);
}
else if(updateCalendarEventMutate.status === "error"){
updateCalendarEventMutate.reset();
addErrorMessage(`Error updating calendar event ${eventName}: ${updateCalendarEventMutate.error.message}`);
console.log(updateCalendarEventMutate.error);
}
else if(deleteCalendarEventMutate.status === "error"){
deleteCalendarEventMutate.reset();
addErrorMessage(`Error deleting calendar event ${eventName}: ${deleteCalendarEventMutate.error.message}`);
console.log(deleteCalendarEventMutate.error);
}
});
const createCalendarEvent = () => {
createCalendarEventMutate.mutate({eventName, eventDescription, eventStartDate, eventEndDate});
}
const updateCalendarEvent = () => {
updateCalendarEventMutate.mutate({calendarEventId: calendarEvent?.calendarEventId, eventName, eventDescription, eventStartDate, eventEndDate});
}
const deleteCalendarEvent = () => {
deleteCalendarEventMutate.mutate(calendarEvent as CalendarEvent);
}
useEffect(() => {
if(calendarEvent){
setEventName(calendarEvent?.eventName ?? "");
setEventDescription(calendarEvent?.eventDescription ?? "");
setEventStartDate(calendarEvent.eventStartDate);
setEventEndDate(calendarEvent.eventEndDate);
}
else{
setEventName("");
setEventDescription("");
setEventStartDate(new Date());
setEventEndDate(new Date());
}
}, [ calendarEvent ]);
return (
<RaidBuilderModal
display={display}
close={close}
modalHeader={calendarEvent?.calendarEventId ? "Update Event" : "Create Event"}
modalBody={
<div
className="flex flex-col items-center justify-center gap-4"
>
<div
className="px-4"
>
<TextInput
id={`calendarEventModalNameInput${modalId}`}
placeholder="Event Name"
onChange={(e) => setEventName(e.target.value)}
value={eventName}
/>
</div>
<TextArea
id={`calendarEventModalDescriptionInput${modalId}`}
placeholder="Event Description"
onChange={(e) => setEventDescription(e.target.value)}
value={eventDescription}
/>
<div
className="w-full"
>
<DateInput
id={`calendarEventModalStartDateInput${modalId}`}
placeholder="Start Date"
value={moment(eventStartDate).format("YYYY-MM-DDTHH:mm")}
onChange={(e) => setEventStartDate(moment(e.target.value).toDate())}
/>
<DateInput
id={`calendarEventModalEndDateInput${modalId}`}
placeholder="End Date"
value={moment(eventEndDate).format("YYYY-MM-DDTHH:mm")}
onChange={(e) => setEventEndDate(moment(e.target.value).toDate())}
/>
</div>
</div>
}
modalFooter={
<div
className="flex flex-row items-center justify-center gap-4 w-full"
>
<div
className="flex flex-row items-center justify-start w-full"
>
&nbsp;
</div>
<div
className="flex flex-row items-center justify-center w-full gap-4"
>
<PrimaryButton
onClick={calendarEvent?.calendarEventId ? updateCalendarEvent : createCalendarEvent}
>
{calendarEvent?.calendarEventId ? "Update" : "Create"}
</PrimaryButton>
<SecondaryButton
onClick={close}
>
Cancel
</SecondaryButton>
</div>
<div
className="flex flex-row items-center justify-end w-full"
>
{
calendarEvent?.calendarEventId &&
<DangerButton
variant="ghost"
shape="square"
onClick={deleteCalendarEvent}
>
<BsTrash3
size={22}
/>
</DangerButton>
}
{
!calendarEvent?.calendarEventId &&
<>&nbsp;</>
}
</div>
</div>
}
/>
);
}

View File

@@ -15,7 +15,7 @@ export default function AllGamesDisplay(){
const [ searchTerm, setSearchTerm ] = useState<string>("");
const [ sentSearchTerm, setSentSearchTerm ] = useState<string>();
const pageSize = 10;
const modalId = crypto.randomUUID().replace("-", "");
const modalId = crypto.randomUUID().replaceAll("-", "");
const gamesCountQuery = useGetGamesCount(sentSearchTerm);

View File

@@ -0,0 +1,61 @@
import { ButtonProps } from "@/components/button/Button";
import { Game } from "@/interface/Game";
import { useState } from "react";
import GameAdminButtons from "./GameAdminButtons";
import DeleteGameModal from "./modals/DeleteGameModal";
import GameModal from "./modals/GameModal";
export default function GameHeader({
game
}:{
game: Game;
}){
const [ displayEditGameModal, setDisplayEditGameModal ] = useState(false);
const [ displayDeleteGameModal, setDisplayDeleteGameModal ] = useState(false);
const buttonProps: ButtonProps = {
variant: "ghost",
size: "md",
shape: "square"
};
return (
<h1
className="flex flex-col items-center justify-center"
>
<div
className="flex flex-row items-center justify-center text-4xl"
>
{
game.gameIcon &&
<img
className="m-auto mr-4"
src={`${import.meta.env.VITE_ICON_URL}/gameIcons/${game.gameIcon}`}
height={72}
width={72}
/>
}
{game.gameName}
</div>
<div>
<GameAdminButtons
buttonProps={buttonProps}
showEditGameModal={() => setDisplayEditGameModal(true)}
showDeleteGameModal={() => setDisplayDeleteGameModal(true)}
/>
</div>
<GameModal
display={displayEditGameModal}
close={() => setDisplayEditGameModal(false)}
game={game}
/>
<DeleteGameModal
display={displayDeleteGameModal}
close={() => setDisplayDeleteGameModal(false)}
game={game}
/>
</h1>
);
}

View File

@@ -21,7 +21,7 @@ export default function GameModal({
const [ gameName, setGameName ] = useState(game?.gameName);
const [ gameIcon, setGameIcon ] = useState(game?.gameIcon);
const [ iconFile, setIconFile ] = useState<File | null>(null);
const modalId = crypto.randomUUID().replace("-", "");
const modalId = crypto.randomUUID().replaceAll("-", "");
useEffect(() => {

View File

@@ -1,20 +1,17 @@
import PrimaryButton from "@/components/button/PrimaryButton";
import TextInput from "@/components/input/TextInput";
import Pagination from "@/components/pagination/Pagination";
import { useGetRaidGroupsCount } from "@/hooks/RaidGroupHooks";
import { useEffect, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import RaidGroupModal from "./modals/RaidGroupModal";
import RaidGroupCreateAndSearch from "./RaidGroupCreateAndSearch";
import RaidGroupsLoader from "./RaidGroupsLoader";
export default function AdminRaidGroupTab(){
const [ displayCreateRaidGroupModal, setDisplayRaidGroupModal ] = useState(false);
export default function AllRaidGroupsDisplay(){
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().replace("-", "");
const raidGroupsCountQuery = useGetRaidGroupsCount(sentSearchTerm);
@@ -37,44 +34,10 @@ export default function AdminRaidGroupTab(){
return (
<>
<div
className="flex flex-row justify-between items-center w-full"
>
<div
className="flex flex-row items-center justify-start w-full"
>
&nbsp;
</div>
{/* Add Raid Group Button */}
<div
className="flex flex-row items-center justify-center w-full"
>
<PrimaryButton
className="mb-8"
onClick={() => setDisplayRaidGroupModal(true)}
>
Create Raid Group
</PrimaryButton>
<RaidGroupModal
display={displayCreateRaidGroupModal}
close={() => setDisplayRaidGroupModal(false)}
raidGroup={undefined}
/>
</div>
{/* Raid Group Search Box */}
<div
className="flex flex-row items-center justify-end w-full"
>
<div>
<TextInput
id={`raidGroupSearchBox${modalId}`}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search"
/>
</div>
</div>
</div>
<RaidGroupCreateAndSearch
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
/>
{/* Raid Group List */}
<RaidGroupsLoader
page={page}

View File

@@ -0,0 +1,58 @@
import PrimaryButton from "@/components/button/PrimaryButton";
import TextInput from "@/components/input/TextInput";
import { useState } from "react";
import RaidGroupModal from "./modals/RaidGroupModal";
export default function RaidGroupCreateAndSearch({
searchTerm,
setSearchTerm
}:{
searchTerm: string;
setSearchTerm: React.Dispatch<React.SetStateAction<string>>;
}){
const [ displayCreateRaidGroupModal, setDisplayRaidGroupModal ] = useState(false);
const modalId = crypto.randomUUID().replaceAll("-", "");
return (
<div
className="flex flex-row justify-between items-center w-full"
>
<div
className="flex flex-row items-center justify-start w-full"
>
&nbsp;
</div>
{/* Add Raid Group Button */}
<div
className="flex flex-row items-center justify-center w-full"
>
<PrimaryButton
className="mb-8"
onClick={() => setDisplayRaidGroupModal(true)}
>
Create Raid Group
</PrimaryButton>
<RaidGroupModal
display={displayCreateRaidGroupModal}
close={() => setDisplayRaidGroupModal(false)}
raidGroup={undefined}
/>
</div>
{/* Raid Group Search Box */}
<div
className="flex flex-row items-center justify-end w-full"
>
<div>
<TextInput
id={`raidGroupSearchBox${modalId}`}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search"
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,66 @@
import Pagination from "@/components/pagination/Pagination";
import { useGetRaidGroupsByGameCount } from "@/hooks/RaidGroupHooks";
import { useEffect, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import RaidGroupCreateAndSearch from "./RaidGroupCreateAndSearch";
import RaidGroupsByGameLoader from "./RaidGroupsByGameLoader";
export default function RaidGroupsByGameDisplay({
gameId
}:{
gameId: string;
}){
const [ page, setPage ] = useState(1);
const [ totalPages, setTotalPages ] = useState(1);
const [ searchTerm, setSearchTerm ] = useState("");
const [ sentSearchTerm, setSentSearchTerm ] = useState<string>();
const pageSize = 10;
const raidGroupsCountQuery = useGetRaidGroupsByGameCount(gameId, sentSearchTerm);
const updateSearchTerm = useDebouncedCallback((newSearchTerm: string) => {
setSentSearchTerm(newSearchTerm.length ? newSearchTerm : undefined);
}, 1000);
useEffect(() => {
updateSearchTerm(searchTerm);
}, [ searchTerm, updateSearchTerm ]);
useEffect(() => {
if(raidGroupsCountQuery.status === "success"){
setTotalPages(Math.ceil(raidGroupsCountQuery.data / pageSize));
}
}, [ raidGroupsCountQuery ]);
return (
<div
className="flex flex-col items-center justify-center"
>
<RaidGroupCreateAndSearch
searchTerm={searchTerm}
setSearchTerm={setSearchTerm}
/>
{/* Raid Group List */}
<RaidGroupsByGameLoader
gameId={gameId ?? ""}
page={page}
pageSize={pageSize}
searchTerm={searchTerm}
/>
{/* Pagination */}
<div
className="my-12"
>
<Pagination
currentPage={page}
totalPages={totalPages}
onChange={setPage}
/>
</div>
</div>
);
}

View File

@@ -0,0 +1,34 @@
import DangerMessage from "@/components/message/DangerMessage";
import { useGetRaidGroupsByGame } from "@/hooks/RaidGroupHooks";
import RaidGroupsList from "./RaidGroupsList";
import RaidGroupsListSkeleton from "./RaidGroupsListSkeleton";
export default function RaidGroupsByGameLoader({
gameId,
page,
pageSize,
searchTerm
}:{
gameId: string;
page: number;
pageSize: number;
searchTerm?: string;
}){
const raidGroupsQuery = useGetRaidGroupsByGame(gameId, page - 1, pageSize, searchTerm);
if(raidGroupsQuery.status === "pending"){
return <RaidGroupsListSkeleton/>
}
else if(raidGroupsQuery.status === "error"){
return <DangerMessage>Error {raidGroupsQuery.error.message}</DangerMessage>
}
else{
return (
<RaidGroupsList
raidGroups={raidGroupsQuery.data ?? []}
/>
);
}
}

View File

@@ -25,7 +25,7 @@ export default function RaidGroupModal({
const [ raidGroupIcon, setRaidGroupIcon ] = useState(raidGroup?.raidGroupIcon);
const [ iconFile, setIconFile ] = useState<File | null>(null);
const [ game, setGame ] = useState<Game>();
const modalId = crypto.randomUUID().replace("-", "");
const modalId = crypto.randomUUID().replaceAll("-", "");
useEffect(() => {

34
src/util/CalendarUtil.ts Normal file
View File

@@ -0,0 +1,34 @@
import { CalendarEvent } from "@/interface/Calendar";
import { EventInput } from "@fullcalendar/core/index.js";
import moment from "moment";
const enum CalendarEventColors {
GAME = "#3788D8",
RAID_GROUP = "#DB301D",
RAID_INSTANCE = "#30BB37"
}
export function calendarEventToFullCalendarEvent(calendarEvents: CalendarEvent[]): EventInput[]{
const newEvents: EventInput[] = [];
for(const calEvent of calendarEvents){
newEvents.push({
id: calEvent.calendarEventId,
title: calEvent.eventName,
start: calEvent.eventStartDate,
end: calEvent.eventEndDate,
backgroundColor: calEvent.gameId ? CalendarEventColors.GAME : calEvent.raidInstanceId ? CalendarEventColors.RAID_INSTANCE : CalendarEventColors.RAID_GROUP,
borderColor: calEvent.gameId ? CalendarEventColors.GAME : calEvent.raidInstanceId ? CalendarEventColors.RAID_INSTANCE : CalendarEventColors.RAID_GROUP
});
}
return newEvents;
}
export const dateToString = (inDate: Date): string => {
return moment(inDate).format("MM/DD/YYYY HH:mm");
}

View File

@@ -1,9 +1,9 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"target": "ES2024",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"lib": ["ES2024", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,