Skip to content

Commit

Permalink
Merge pull request #144 from OxfordRSE/duplicate-event
Browse files Browse the repository at this point in the history
Duplicate event
martinjrobins authored Jan 24, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
2 parents b10b9e0 + df29357 commit 054b8a0
Showing 7 changed files with 197 additions and 20 deletions.
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
@@ -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
@@ -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)
@@ -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) => {
@@ -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>
8 changes: 7 additions & 1 deletion components/Overlay.tsx
Original file line number Diff line number Diff line change
@@ -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
@@ -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 = () => {
@@ -64,6 +65,10 @@ const Overlay: NextPage<Props> = ({
setShowDeleteEventModal(false)
}

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

const handleClose = () => {
setSidebarOpen(false)
}
@@ -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>
13 changes: 10 additions & 3 deletions components/forms/DateTimeField.tsx
Original file line number Diff line number Diff line change
@@ -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
@@ -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>
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())
12 changes: 10 additions & 2 deletions pages/api/event.ts
Original file line number Diff line number Diff line change
@@ -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()
4 changes: 0 additions & 4 deletions pages/api/event/[eventId].ts
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 054b8a0

Please sign in to comment.