Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Duplicate event #144

Merged
merged 3 commits into from
Jan 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions components/DuplicateEventModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import React, { useEffect } from "react"
import { useState } from "react"
import { Button, Modal } from "flowbite-react"
import { atom, useRecoilState } from "recoil"
import useEvent from "lib/hooks/useEvent"
import useEvents from "lib/hooks/useEvents"
import { postEvent } from "lib/actions/postEvent"
import { HiCheckCircle } from "react-icons/hi"
import { useForm } from "react-hook-form"
import { putEvent } from "lib/actions/putEvent"
import DateTimeField from "./forms/DateTimeField"
import { Stack } from "@mui/material"
import { EventItem } from "pages/api/eventGroup/[eventGroupId]"
import { Toast } from "flowbite-react"

interface DuplicateEventProps {
onClose: () => void
}

interface DuplicateEventForm {
date: Date
}

export const duplicateEventModalState = atom({
key: "duplicateEventModalState",
default: false,
})

export const duplicateEventIdState = atom<number>({
key: "duplicateEventIdState",
default: undefined,
})

export const DuplicateEventModal: React.FC<DuplicateEventProps> = ({ onClose }) => {
const [showDuplicateEventModal, setShowDuplicateEventModal] = useRecoilState(duplicateEventModalState)
const [duplicateEventId, setDuplicateEventId] = useRecoilState(duplicateEventIdState)
const { events: currentEvents, mutate } = useEvents()
const { event } = useEvent(duplicateEventId)
const { events, mutate: mutateEvents } = useEvents()
const [success, setSuccess] = useState<string | null>(null)
const { control, handleSubmit, reset } = useForm<DuplicateEventForm>({ defaultValues: { date: event?.start } })

useEffect(() => {
reset({ date: event?.start })
}, [event])

const duplicateEventGroup = (eg: any, dateOffset: number) => {
let newEg = JSON.parse(JSON.stringify(eg))
newEg.id = undefined
newEg.EventItem = []
newEg.start = new Date(new Date(newEg.start).getTime() + dateOffset)
newEg.end = new Date(new Date(newEg.end).getTime() + dateOffset)
eg.EventItem.map((ei: EventItem) => {
const newEi = duplicateEventItem(ei)
newEg.EventItem.push(newEi)
})
return newEg
}

const duplicateEventItem = (ei: any) => {
let newEi = JSON.parse(JSON.stringify(ei))
newEi.id = undefined
newEi.groupId = undefined
return newEi
}

const duplicateEvent = (data: DuplicateEventForm) => {
// make a new event
if (!event) return
const newDate = data.date
const dateOffset = new Date(newDate).getTime() - new Date(event.start).getTime()

let eventDuplicate = JSON.parse(JSON.stringify(event))
// remove EG and UOE and id to prevent them being duplicated with duplicate ids
eventDuplicate.EventGroup = []
eventDuplicate.UserOnEvent = []
eventDuplicate.id = undefined
eventDuplicate.start = new Date(new Date(eventDuplicate.start).getTime() + dateOffset)
eventDuplicate.end = new Date(new Date(eventDuplicate.end).getTime() + dateOffset)

postEvent(eventDuplicate).then((newEvent) => {
// here we take the newly saved event and add copies of the eventgroups and items
newEvent.EventGroup = []
event.EventGroup.map((eg) => {
const newEg = duplicateEventGroup(eg, dateOffset)
newEvent.EventGroup.push(newEg)
})
// prevent createmany from failing
newEvent.UserOnEvent = []

putEvent(newEvent).then((updatedEvent) => {
const finalEvent = updatedEvent.event
// we ignore the ts error because it doesn't seem to understand the input for mutate here
// @ts-ignore
mutateEvents([...events, finalEvent])
setSuccess("success")
setTimeout(() => {
setShowDuplicateEventModal(false)
onClose()
setSuccess(null)
}, 1500)
})
})
}

return (
<Modal dismissible={true} show={showDuplicateEventModal} onClose={onClose} size="xl">
<Modal.Header>Duplicate Event</Modal.Header>
<Modal.Body>
<Stack direction="column" spacing="0.4rem">
<p>This will create a duplicate of event: {event?.name}</p>
<p>
Enter a start date for the event. All dates and times will be appropriately adjusted relative to the start
date but you will likely need to further adjust the schedule to suit the specifics of the course.
</p>
<DateTimeField name={"date"} control={control} />
<Button
size="sm"
className="m-0 h-10 mt-1"
onClick={handleSubmit(duplicateEvent)}
data-cy="confirm-event-duplicate"
>
Create Duplicate Event
</Button>
{success && (
<Toast className="">
<div className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-green-100 text-green-500 dark:bg-green-800 dark:text-green-200">
<HiCheckCircle className="h-5 w-5" />
</div>
<div className="ml-3 text-sm font-normal">Event Successfully Duplicated!</div>
</Toast>
)}
</Stack>
</Modal.Body>
</Modal>
)
}
38 changes: 30 additions & 8 deletions components/EventsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import useEvents from "lib/hooks/useEvents"
import useProfile from "lib/hooks/useProfile"
import useActiveEvent from "lib/hooks/useActiveEvents"
import { postEvent } from "lib/actions/postEvent"
import { MdDelete } from "react-icons/md"
import { MdContentCopy, MdDelete } from "react-icons/md"
import { useRecoilState } from "recoil"
import { deleteEventModalState, deleteEventIdState, DeleteEventModal } from "components/deleteEventModal"
import { deleteEventModalState, deleteEventIdState } from "components/deleteEventModal"
import { duplicateEventModalState, duplicateEventIdState } from "components/DuplicateEventModal"
import { Tooltip } from "@mui/material"
import Stack from "./ui/Stack"

type EventsProps = {
material: Material
Expand All @@ -22,6 +25,8 @@ const EventsView: React.FC<EventsProps> = ({ material, events }) => {
const [showDateTime, setShowDateTime] = useState(false)
const [showDeleteEventModal, setShowDeleteEventModal] = useRecoilState(deleteEventModalState)
const [deleteEventId, setDeleteEventId] = useRecoilState(deleteEventIdState)
const [showDuplicateEventModal, setShowDuplicateEventModal] = useRecoilState(duplicateEventModalState)
const [duplicateEventId, setDuplicateEventId] = useRecoilState(duplicateEventIdState)

useEffect(() => {
setShowDateTime(true)
Expand Down Expand Up @@ -58,6 +63,11 @@ const EventsView: React.FC<EventsProps> = ({ material, events }) => {
setDeleteEventId(eventId)
}

const openDuplicateEventModal = (eventId: number) => {
setShowDuplicateEventModal(true)
setDuplicateEventId(eventId)
}

return (
<Timeline>
{events.map((event) => {
Expand All @@ -70,12 +80,24 @@ const EventsView: React.FC<EventsProps> = ({ material, events }) => {
{showDateTime && event.start.toLocaleString([], { dateStyle: "medium", timeStyle: "short" })}
</Link>
{isAdmin && (
<MdDelete
className="ml-2 inline text-red-500 flex cursor-pointer"
data-cy={`delete-event-${event.id}`}
size={18}
onClick={() => openDeleteEventModal(event.id)}
/>
<Stack direction="row">
<Tooltip title="Duplicate Event">
<MdContentCopy
className="ml-2 inline flex cursor-pointer"
data-cy={`duplicate-event-${event.id}`}
size={18}
onClick={() => openDuplicateEventModal(event.id)}
/>
</Tooltip>
<Tooltip title="Delete Event">
<MdDelete
className="ml-2 inline text-red-500 flex cursor-pointer"
data-cy={`delete-event-${event.id}`}
size={18}
onClick={() => openDeleteEventModal(event.id)}
/>
</Tooltip>
</Stack>
)}
</Timeline.Time>
<Timeline.Title>
Expand Down
8 changes: 7 additions & 1 deletion components/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Sidebar from "./Sidebar"
import { SearchDialog, searchQueryState } from "components/SearchDialog"
import { useRecoilState } from "recoil"
import { DeleteEventModal, deleteEventModalState } from "components/deleteEventModal"
import { enableSearch } from "lib/search/enableSearch"
import { DuplicateEventModal, duplicateEventModalState } from "components/DuplicateEventModal"

interface Props {
material: Material
Expand Down Expand Up @@ -41,6 +41,7 @@ const Overlay: NextPage<Props> = ({
const [showSearch, setShowSearch] = useRecoilState(searchQueryState)
const [showTopButtons, setShowTopButtons] = useState(false)
const [showDeleteEventModal, setShowDeleteEventModal] = useRecoilState(deleteEventModalState)
const [showDuplicateEventModal, setShowDuplicateEventModal] = useRecoilState(duplicateEventModalState)

useEffect(() => {
const handleScroll = () => {
Expand All @@ -64,6 +65,10 @@ const Overlay: NextPage<Props> = ({
setShowDeleteEventModal(false)
}

const closeDuplicateEvent = () => {
setShowDuplicateEventModal(false)
}

const handleClose = () => {
setSidebarOpen(false)
}
Expand Down Expand Up @@ -108,6 +113,7 @@ const Overlay: NextPage<Props> = ({
<AttributionDialog citations={attribution} isOpen={showAttribution} onClose={closeAttribution} />
<SearchDialog onClose={closeSearch} />
<DeleteEventModal onClose={closeDeleteEvent} />
<DuplicateEventModal onClose={closeDuplicateEvent} />
<Sidebar material={material} activeEvent={activeEvent} sidebarOpen={sidebarOpen} handleClose={handleClose} />
</div>
</div>
Expand Down
13 changes: 10 additions & 3 deletions components/forms/DateTimeField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,16 @@ type Props<T extends FieldValues> = {
name: FieldPath<T>
control: Control<T>
rules?: Object
defaultValue?: Date
}

function DateTimeField<T extends FieldValues>({ label, name, control, rules }: Props<T>): React.ReactElement {
function DateTimeField<T extends FieldValues>({
label,
name,
control,
rules,
defaultValue,
}: Props<T>): React.ReactElement {
const labelId = `${name}-label`
return (
<Controller
Expand All @@ -32,9 +39,9 @@ function DateTimeField<T extends FieldValues>({ label, name, control, rules }: P
format="YYYY-MM-DD HH:mm"
name={name}
ampm={false}
className="font-normal bg-grey-100 dark:bg-gray-700 dark:text-gray-200"
className="font-normal bg-grey-100 dark:bg-gray-600 dark:text-gray-200"
value={dayjs(value)}
slotProps={{ openPickerIcon: { className: "bg-grey-100 dark:bg-gray-700 dark:text-gray-200" } }}
slotProps={{ openPickerIcon: { className: "bg-grey-100 dark:bg-gray-600 dark:text-gray-200" } }}
onChange={onChange}
/>
</LocalizationProvider>
Expand Down
5 changes: 3 additions & 2 deletions lib/actions/postEvent.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { basePath } from "lib/basePath"
import { Event } from "lib/types"
import { Event } from "pages/api/event/[eventId]"

// POST /api/events
export const postEvent = async (): Promise<Event> => {
export const postEvent = async (data?: Event): Promise<Event> => {
const apiPath = `${basePath}/api/event`
const requestOptions = {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
}
return fetch(apiPath, requestOptions)
.then((response) => response.json())
Expand Down
12 changes: 10 additions & 2 deletions pages/api/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,16 @@ const Events = async (req: NextApiRequest, res: NextApiResponse<Data>) => {
res.status(403).json({ error: "Forbidden" })
return
}
const event = await prisma.event.create({ data: {} })
res.status(201).json({ event: event })
if (!req.body) {
const event = await prisma.event.create({ data: {} })
res.status(201).json({ event: event })
} else {
req.body.UserOnEvent = undefined
req.body.EventGroup = undefined
req.body.id = undefined
const event = await prisma.event.create({ data: req.body })
res.status(201).json({ event: event })
}
} else if (req.method === "GET") {
if (isAdmin) {
const events: Event[] = await prisma.event.findMany()
Expand Down
4 changes: 0 additions & 4 deletions pages/api/event/[eventId].ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,6 @@ const eventHandler = async (req: NextApiRequest, res: NextApiResponse<Data>) =>
const eventGroupData: EventGroup[] = req.body.event.EventGroup
const userOnEventData: UserOnEvent[] = req.body.event.UserOnEvent

console.log("eventGroupData", eventGroupData)
console.log("userOnEventData", userOnEventData)
console.log("req.body", req.body)

if (!isAdmin) {
res.status(401).json({ error: "Unauthorized" })
return
Expand Down