-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
801 frontend add interface for admins to create new events (#804)
* working base implementation * add tests * pull up storage service logic * update docs * remove date picker spacing between * add dividers * add use client
- Loading branch information
1 parent
e275f84
commit a9f3b2c
Showing
12 changed files
with
500 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,11 @@ | ||
"use client" | ||
|
||
import WorkInProgressComponent from "@/components/generic/WorkInProgressComponent/WorkInProgressComponent" | ||
import { AdminHeading } from "../AdminHeading" | ||
import WrappedAdminEventView from "@/components/composite/Admin/AdminEventView/WrappedAdminEventView" | ||
|
||
export default function AdminEventsPage() { | ||
return ( | ||
<> | ||
<AdminHeading title="Events" /> | ||
<div className="fixed flex h-screen w-full flex-col items-center justify-center gap-4"> | ||
<WorkInProgressComponent pageName="Admin Events" /> | ||
<p className="text-light-blue-100">Work in progress</p> | ||
</div> | ||
<WrappedAdminEventView /> | ||
</> | ||
) | ||
} |
11 changes: 11 additions & 0 deletions
11
client/src/components/composite/Admin/AdminEventView/AdminEventForm/AdminEventForm.story.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import type { Meta, StoryObj } from "@storybook/react" | ||
import AdminEventForm from "./AdminEventForm" | ||
|
||
const meta: Meta<typeof AdminEventForm> = { | ||
component: AdminEventForm | ||
} | ||
|
||
export default meta | ||
type Story = StoryObj<typeof meta> | ||
|
||
export const DefaultAdminEventForm: Story = {} |
68 changes: 68 additions & 0 deletions
68
client/src/components/composite/Admin/AdminEventView/AdminEventForm/AdminEventForm.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import React from "react" | ||
import { render, fireEvent, waitFor } from "@testing-library/react" | ||
import AdminEventForm, { AdminEventFormKeys } from "./AdminEventForm" | ||
|
||
let handlePostEvent: any | ||
const setup = () => { | ||
handlePostEvent = jest.fn() | ||
const utils = render( | ||
<AdminEventForm | ||
handlePostEvent={handlePostEvent} | ||
generateImageLink={async () => { | ||
return undefined | ||
}} | ||
/> | ||
) | ||
return { | ||
...utils | ||
} | ||
} | ||
|
||
describe("AdminEventForm", () => { | ||
let confirmSpy: any | ||
beforeAll(() => { | ||
confirmSpy = jest.spyOn(window, "confirm") | ||
confirmSpy.mockImplementation(jest.fn(() => true)) | ||
}) | ||
afterAll(() => confirmSpy.mockRestore()) | ||
|
||
test("does not submit with invalid fields", async () => { | ||
const { getByTestId } = setup() | ||
|
||
fireEvent.click(getByTestId("post-event-button")) | ||
|
||
await waitFor(() => { | ||
expect(handlePostEvent).not.toHaveBeenCalled() | ||
}) | ||
}) | ||
|
||
test("submits the form with required data", async () => { | ||
const { getByTestId } = setup() | ||
|
||
fireEvent.change(getByTestId(AdminEventFormKeys.TITLE), { | ||
target: { value: "Test Event" } | ||
}) | ||
fireEvent.change(getByTestId(AdminEventFormKeys.SIGN_UP_START_DATE), { | ||
target: { value: "2024-10-08T10:00" } | ||
}) | ||
fireEvent.change(getByTestId(AdminEventFormKeys.PHYSICAL_START_DATE), { | ||
target: { value: "2024-10-09T10:00" } | ||
}) | ||
fireEvent.change(getByTestId(AdminEventFormKeys.LOCATION), { | ||
target: { value: "Test Location" } | ||
}) | ||
|
||
fireEvent.click(getByTestId("post-event-button")) | ||
|
||
await waitFor(() => { | ||
expect(handlePostEvent).toHaveBeenCalledWith({ | ||
data: expect.objectContaining({ | ||
title: "Test Event", | ||
sign_up_start_date: expect.any(Object), | ||
physical_start_date: expect.any(Object), | ||
location: "Test Location" | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) |
205 changes: 205 additions & 0 deletions
205
client/src/components/composite/Admin/AdminEventView/AdminEventForm/AdminEventForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,205 @@ | ||
import Button from "@/components/generic/FigmaButtons/FigmaButton" | ||
import TextInput from "@/components/generic/TextInputComponent/TextInput" | ||
import { CreateEventBody } from "@/models/Events" | ||
import { Timestamp } from "firebase/firestore" | ||
import Image from "next/image" | ||
import { FormEvent, useState } from "react" | ||
|
||
interface IAdminEventForm { | ||
/** | ||
* Pass in **only** if using to edit instead of create. | ||
* If it is not `undefined` assume that it is event edit instead of creation | ||
*/ | ||
eventId?: string | ||
|
||
/** | ||
* To generate a link to be stored in firestore | ||
* | ||
* @param image The uploaded image to store | ||
*/ | ||
generateImageLink: (image: File) => Promise<string | undefined> | ||
|
||
/** | ||
* To be called after user submits the new data for the event | ||
*/ | ||
handlePostEvent: (data: CreateEventBody) => void | ||
} | ||
|
||
export const AdminEventFormKeys = { | ||
TITLE: "title", | ||
DESCRIPTION: "description", | ||
IMAGE_URL: "image url", | ||
LOCATION: "location", | ||
GOOGLE_FORMS_LINK: "google forms link", | ||
SIGN_UP_START_DATE: "sign up start date", | ||
SIGN_UP_END_DATE: "sign up end date", | ||
PHYSICAL_START_DATE: "physical start date", | ||
PHYSICAL_END_DATE: "physical end date", | ||
MAX_OCCUPANCY: "max signups" | ||
} as const | ||
|
||
const Divider = () => <span className="bg-gray-3 my-3 h-[1px] w-full" /> | ||
|
||
const AdminEventForm = ({ | ||
handlePostEvent, | ||
generateImageLink | ||
}: IAdminEventForm) => { | ||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false) | ||
|
||
const [uploadedImage, setUploadedImage] = useState<File | undefined>() | ||
|
||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { | ||
e.preventDefault() | ||
const data = new FormData(e.currentTarget) | ||
|
||
const physical_end_date = data.get(AdminEventFormKeys.PHYSICAL_END_DATE) | ||
const sign_up_end_date = data.get(AdminEventFormKeys.SIGN_UP_END_DATE) | ||
|
||
const body: CreateEventBody["data"] = { | ||
// Required Fields | ||
title: data.get(AdminEventFormKeys.TITLE) as string, | ||
sign_up_start_date: Timestamp.fromDate( | ||
new Date(data.get(AdminEventFormKeys.SIGN_UP_START_DATE) as string) | ||
), | ||
physical_start_date: Timestamp.fromDate( | ||
new Date(data.get(AdminEventFormKeys.PHYSICAL_START_DATE) as string) | ||
), | ||
// Optional fields | ||
google_forms_link: data.get( | ||
AdminEventFormKeys.GOOGLE_FORMS_LINK | ||
) as string, | ||
sign_up_end_date: sign_up_end_date | ||
? Timestamp.fromDate(new Date(sign_up_end_date as string)) | ||
: undefined, | ||
location: (data.get(AdminEventFormKeys.LOCATION) as string) || "", | ||
max_occupancy: | ||
Number.parseInt(data.get(AdminEventFormKeys.MAX_OCCUPANCY) as string) || | ||
undefined, | ||
physical_end_date: physical_end_date | ||
? Timestamp.fromDate(new Date(physical_end_date as string)) | ||
: undefined | ||
} | ||
|
||
try { | ||
setIsSubmitting(true) | ||
|
||
if ( | ||
confirm( | ||
`Are you sure you want to create the new event with title ${body.title}?` | ||
) | ||
) { | ||
handlePostEvent({ | ||
data: { | ||
...body, | ||
image_url: uploadedImage && (await generateImageLink(uploadedImage)) | ||
} | ||
}) | ||
} | ||
} finally { | ||
setIsSubmitting(false) | ||
} | ||
} | ||
return ( | ||
<div className="relative my-4 flex w-full flex-col items-center rounded-md bg-white p-2"> | ||
<h2 className="text-dark-blue-100">Create Event</h2> | ||
<form | ||
onSubmit={handleSubmit} | ||
className="flex w-full max-w-[800px] flex-col gap-2" | ||
> | ||
<TextInput | ||
name={AdminEventFormKeys.TITLE} | ||
type="text" | ||
label="Title" | ||
data-testid={AdminEventFormKeys.TITLE} | ||
required | ||
/> | ||
<label htmlFor={AdminEventFormKeys.DESCRIPTION}>Description</label> | ||
<textarea | ||
name={AdminEventFormKeys.DESCRIPTION} | ||
data-testid={AdminEventFormKeys.DESCRIPTION} | ||
/> | ||
|
||
<TextInput | ||
label="Image" | ||
type="file" | ||
accept="image/png,image/jpeg" | ||
onChange={(e) => { | ||
if (e.target.files) { | ||
setUploadedImage(e.target.files[0]) | ||
} | ||
}} | ||
/> | ||
{uploadedImage && ( | ||
<div className=""> | ||
<Image | ||
height={500} | ||
width={500} | ||
src={URL.createObjectURL(uploadedImage)} | ||
alt="Uploaded" | ||
className="max-h-[200px] w-auto" | ||
/> | ||
</div> | ||
)} | ||
|
||
<TextInput | ||
name={AdminEventFormKeys.LOCATION} | ||
type="text" | ||
label="Location" | ||
data-testid={AdminEventFormKeys.LOCATION} | ||
required | ||
/> | ||
<Divider /> | ||
<h3 className="text-dark-blue-100 text-center">Sign up dates</h3> | ||
<span className="flex w-full flex-col gap-2 sm:flex-row"> | ||
<TextInput | ||
name={AdminEventFormKeys.SIGN_UP_START_DATE} | ||
data-testid={AdminEventFormKeys.SIGN_UP_START_DATE} | ||
type="datetime-local" | ||
label="Sign Up Start Date" | ||
required | ||
/> | ||
<TextInput | ||
name={AdminEventFormKeys.SIGN_UP_END_DATE} | ||
data-testid={AdminEventFormKeys.SIGN_UP_END_DATE} | ||
type="datetime-local" | ||
label="Sign Up End Date (If exists)" | ||
/> | ||
</span> | ||
|
||
<Divider /> | ||
<h3 className="text-dark-blue-100 mt-2 text-center">Event Dates</h3> | ||
<span className="flex w-full flex-col gap-2 sm:flex-row"> | ||
<TextInput | ||
name={AdminEventFormKeys.PHYSICAL_START_DATE} | ||
data-testid={AdminEventFormKeys.PHYSICAL_START_DATE} | ||
type="datetime-local" | ||
label="Start Date of Event" | ||
required | ||
/> | ||
<TextInput | ||
name={AdminEventFormKeys.PHYSICAL_END_DATE} | ||
data-testid={AdminEventFormKeys.PHYSICAL_END_DATE} | ||
type="datetime-local" | ||
label="End Date of Event" | ||
/> | ||
</span> | ||
<Divider /> | ||
<TextInput | ||
name={AdminEventFormKeys.GOOGLE_FORMS_LINK} | ||
data-testid={AdminEventFormKeys.GOOGLE_FORMS_LINK} | ||
type="url" | ||
label="Google Forms Link" | ||
/> | ||
<Button | ||
disabled={isSubmitting} | ||
type="submit" | ||
data-testid="post-event-button" | ||
> | ||
Add Event | ||
</Button> | ||
</form> | ||
</div> | ||
) | ||
} | ||
|
||
export default AdminEventForm |
19 changes: 19 additions & 0 deletions
19
client/src/components/composite/Admin/AdminEventView/AdminEventView.story.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import type { Meta, StoryObj } from "@storybook/react" | ||
|
||
import AdminEventView from "./AdminEventView" | ||
|
||
const meta: Meta<typeof AdminEventView> = { | ||
component: AdminEventView | ||
} | ||
|
||
export default meta | ||
type Story = StoryObj<typeof meta> | ||
|
||
export const DefaultAdminEventView: Story = { | ||
args: { | ||
handlePostEvent: () => {}, | ||
generateImageLink: async () => { | ||
return undefined | ||
} | ||
} | ||
} |
Oops, something went wrong.