implemented modifing of tasks of an event

This commit is contained in:
z1glr
2025-01-13 22:22:10 +00:00
parent 0685283007
commit a3c6fd685d
12 changed files with 468 additions and 288 deletions

View File

@@ -1,33 +1,7 @@
import { useEffect, useReducer, useState } from "react";
import { Add } from "@carbon/icons-react";
import zustand from "../../Zustand";
import { getLocalTimeZone, now, ZonedDateTime } from "@internationalized/date";
import {
Button,
Checkbox,
CheckboxGroup,
DatePicker,
Form,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Spinner,
Textarea,
} from "@nextui-org/react";
import { apiCall, getTasks, Task } from "@/lib";
interface state {
date: ZonedDateTime;
description: string;
tasks: string[];
}
interface dispatchAction {
action: "set" | "reset";
value?: Partial<state>;
}
import { Button } from "@nextui-org/react";
import EditEvent, { EventSubmitData } from "./EditEvent";
import { apiCall } from "@/lib";
import { AddLarge } from "@carbon/icons-react";
export default function AddEvent(props: {
className?: string;
@@ -35,138 +9,32 @@ export default function AddEvent(props: {
onOpenChange: (isOpen: boolean) => void;
onSuccess?: () => void;
}) {
// initial state for the inputs
const initialState: state = {
date: now(getLocalTimeZone()),
description: "",
tasks: [],
};
// handle state dispatches
function reducer(state: state, action: dispatchAction): state {
if (action.action === "reset") {
return initialState;
} else {
return { ...state, ...action.value };
}
}
const [state, dispatchState] = useReducer(reducer, initialState);
const [tasks, setTasks] = useState<Record<number, Task>>({});
// get the available tasks
useEffect(() => {
(async () => {
setTasks(await getTasks());
})();
}, []);
// sends the addEvent request to the backend
async function addEvent() {
const data = {
...state,
tasks: state.tasks.map((task) => parseInt(task)),
date: state.date.toAbsoluteString().slice(0, -1),
};
async function addEvent(data: EventSubmitData) {
const result = await apiCall("POST", "events", undefined, data);
if (result.ok) {
zustand.getState().setEvents(await result.json());
props.onOpenChange(false);
props.onSuccess?.();
}
}
// reset the state when the modal gets closed
useEffect(() => {
if (!props.isOpen) {
dispatchState({ action: "reset" });
}
}, [props.isOpen]);
return (
<Modal
isOpen={props.isOpen}
shadow={"none" as "sm"} // somehow "none" isn't allowed
onOpenChange={props.onOpenChange}
backdrop="blur"
classNames={{
base: "bg-accent-5 ",
}}
<EditEvent
{...props}
onSubmit={(data) => void addEvent(data)}
footer={
<Button
color="primary"
radius="full"
startContent={<AddLarge />}
type="submit"
>
Add
</Button>
}
>
<Form
validationBehavior="native"
onSubmit={(e) => {
e.preventDefault();
void addEvent();
}}
>
<ModalContent>
<ModalHeader>
<h1 className="text-center text-2xl">Add Event</h1>
</ModalHeader>
<ModalBody>
<DatePicker
isRequired
label="Event date"
name="date"
variant="bordered"
hideTimeZone
granularity="minute"
value={state.date}
onChange={(dt) =>
!!dt
? dispatchState({ action: "set", value: { date: dt } })
: null
}
/>
<Textarea
variant="bordered"
placeholder="Description"
name="description"
value={state.description}
onValueChange={(s) =>
dispatchState({ action: "set", value: { description: s } })
}
/>
<CheckboxGroup
value={state.tasks}
name="tasks"
onValueChange={(s) =>
dispatchState({ action: "set", value: { tasks: s } })
}
validate={(value) =>
value.length > 0 ? true : "Atleast one task must be selected"
}
>
{tasks !== undefined ? (
Object.entries(tasks)
.filter(([, task]) => !task.disabled)
.map(([id, task]) => (
<div key={id}>
<Checkbox value={id}>{task.text}</Checkbox>
</div>
))
) : (
<Spinner label="Loading" />
)}
</CheckboxGroup>
</ModalBody>
<ModalFooter>
<Button
color="primary"
radius="full"
startContent={<Add size={32} />}
type="submit"
>
Add
</Button>
</ModalFooter>
</ModalContent>
</Form>
</Modal>
Add Event
</EditEvent>
);
}

View File

@@ -0,0 +1,202 @@
import React, { useEffect, useReducer, useState } from "react";
import {
getLocalTimeZone,
now,
parseDateTime,
toZoned,
ZonedDateTime,
} from "@internationalized/date";
import {
Checkbox,
CheckboxGroup,
DatePicker,
Form,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
Spinner,
Textarea,
} from "@nextui-org/react";
import { getTasks, Task } from "@/lib";
import { EventData } from "@/Zustand";
export interface EventSubmitData {
id: number;
date: string;
description: string;
tasks: number[];
}
interface State {
date: ZonedDateTime;
description: string;
tasks: string[];
}
export default function EditEvent(props: {
children: React.ReactNode;
footer: React.ReactNode;
initialState?: EventData;
className?: string;
isOpen: boolean;
onOpenChange: (isOpen: boolean) => void;
onSubmit?: (data: EventSubmitData) => void;
}) {
const [reverseTasksMap, setReverseTasksMap] = useState<
Record<string, string>
>({});
const [state, dispatchState] = useReducer(
dispatchStateHandler,
dispatchStateHandler({} as State, { action: "reset" }),
);
const [tasksMap, setTasksMap] = useState<Record<number, Task>>({});
// initialize the state
function initialState(): State {
if (props.initialState !== undefined && reverseTasksMap !== undefined) {
const { description, date, tasks } = props.initialState;
return {
description,
date: toZoned(parseDateTime(date), getLocalTimeZone()),
tasks: Object.keys(tasks).map((task) => reverseTasksMap[task]),
};
} else {
return {
date: now(getLocalTimeZone()),
description: "",
tasks: [],
};
}
}
// update the state if the initialState-prop changes
useEffect(() => {
if (props.initialState !== undefined) {
dispatchState({ action: "reset" });
}
}, [props.initialState]);
// handle dispatch-calls
function dispatchStateHandler(
state: State,
args: { action: "patch" | "reset"; value?: Partial<State> },
): State {
if (args.action === "reset") {
return initialState();
} else {
return {
...state,
...args.value,
};
}
}
// shortcut for patching the state
function patchState(values: Partial<State>) {
dispatchState({ action: "patch", value: values });
}
// handle state dispatches
// get the available tasks and initialize the state with them
useEffect(() => {
(async () => {
const tasks = await getTasks();
setTasksMap(tasks);
setReverseTasksMap(
Object.fromEntries(
Object.entries(tasks).map(([id, task]) => {
return [task.text, id];
}),
),
);
})();
}, []);
// sends the patch-event-request to the backend
function patchEvent() {
if (props.initialState !== undefined) {
const { description, tasks, date } = state;
const data: EventSubmitData = {
id: props.initialState?.id,
description,
tasks: tasks.map((task) => parseInt(task)),
date: date.toAbsoluteString().slice(0, -1),
};
props.onSubmit?.(data);
}
}
return (
<Modal
isOpen={props.isOpen}
shadow={"none" as "sm"} // somehow "none" isn't allowed
onOpenChange={props.onOpenChange}
backdrop="blur"
classNames={{
base: "bg-accent-5 ",
}}
>
<Form
validationBehavior="native"
onSubmit={(e) => {
e.preventDefault();
void patchEvent();
}}
>
<ModalContent>
<ModalHeader>
<h1 className="text-center text-2xl">{props.children}</h1>
</ModalHeader>
<ModalBody>
<DatePicker
isRequired
label="Event date"
name="date"
variant="bordered"
hideTimeZone
granularity="minute"
value={state.date}
onChange={(date) => (!!date ? patchState({ date }) : null)}
/>
<Textarea
variant="bordered"
placeholder="Description"
name="description"
value={state.description}
onValueChange={(description) => patchState({ description })}
/>
<CheckboxGroup
name="tasks"
value={state.tasks}
onValueChange={(tasks) => patchState({ tasks })}
validate={(value) =>
value.length > 0 ? true : "Atleast one task must be selected"
}
>
{tasksMap !== undefined ? (
Object.entries(tasksMap)
.filter(([, task]) => !task.disabled)
.map(([id, task]) => (
<div key={id}>
<Checkbox value={id}>{task.text}</Checkbox>
</div>
))
) : (
<Spinner label="Loading" />
)}
</CheckboxGroup>
</ModalBody>
<ModalFooter>{props.footer}</ModalFooter>
</ModalContent>
</Form>
</Modal>
);
}

View File

@@ -5,7 +5,7 @@ import { getLocalTimeZone, parseDateTime } from "@internationalized/date";
import { useLocale } from "@react-aria/i18n";
export default function LocalDate(props: {
children: string;
children?: string;
className?: string;
options: Intl.DateTimeFormatOptions;
}) {
@@ -13,9 +13,11 @@ export default function LocalDate(props: {
return (
<span className={props.className}>
{formatter.format(
parseDateTime(props.children).toDate(getLocalTimeZone()),
)}
{props.children !== undefined
? formatter.format(
parseDateTime(props.children).toDate(getLocalTimeZone()),
)
: ""}
</span>
);
}