From f412444a3dbe422a248e44ca4080f9da73fe7113 Mon Sep 17 00:00:00 2001 From: Sophia Zhu Date: Tue, 14 May 2024 17:26:34 -0700 Subject: [PATCH 01/17] update events creator sidebar --- backend/src/controllers/eventDetails.ts | 3 +- backend/src/models/eventDetails.ts | 1 + backend/src/validators/eventDetails.ts | 8 + frontend/src/api/eventDetails.ts | 20 +- .../app/admin/event-creator/page.module.css | 68 +++ frontend/src/app/admin/event-creator/page.tsx | 389 +++++++++++++++++ .../src/components/EventSidebar.module.css | 191 +++++++++ frontend/src/components/EventSidebar.tsx | 401 ++++++++++++++++++ 8 files changed, 1077 insertions(+), 4 deletions(-) create mode 100644 frontend/src/app/admin/event-creator/page.module.css create mode 100644 frontend/src/app/admin/event-creator/page.tsx create mode 100644 frontend/src/components/EventSidebar.module.css create mode 100644 frontend/src/components/EventSidebar.tsx diff --git a/backend/src/controllers/eventDetails.ts b/backend/src/controllers/eventDetails.ts index b0af355d..68d206b5 100644 --- a/backend/src/controllers/eventDetails.ts +++ b/backend/src/controllers/eventDetails.ts @@ -35,7 +35,7 @@ export const getEventDetails: RequestHandler = async (req, res, next) => { export const createEventDetails: RequestHandler = async (req, res, next) => { const errors = validationResult(req); - const { name, description, guidelines, date, location, imageURI } = req.body; + const { name, description, content, guidelines, date, location, imageURI } = req.body; try { validationErrorParser(errors); @@ -43,6 +43,7 @@ export const createEventDetails: RequestHandler = async (req, res, next) => { const eventDetails = await EventDetails.create({ name, description, + content, guidelines, date, location, diff --git a/backend/src/models/eventDetails.ts b/backend/src/models/eventDetails.ts index e6d47ac7..0d346323 100644 --- a/backend/src/models/eventDetails.ts +++ b/backend/src/models/eventDetails.ts @@ -3,6 +3,7 @@ import { InferSchemaType, Schema, model } from "mongoose"; const eventDetailsSchema = new Schema({ name: { type: String, required: true }, description: { type: String, required: true }, + content: { type: String, required: true }, guidelines: { type: String, required: true }, date: { type: String, required: true }, location: { type: String, required: true }, diff --git a/backend/src/validators/eventDetails.ts b/backend/src/validators/eventDetails.ts index 91ec8450..1dc046cc 100644 --- a/backend/src/validators/eventDetails.ts +++ b/backend/src/validators/eventDetails.ts @@ -21,6 +21,13 @@ const makeDescriptionValidator = () => .bail() .isString() .withMessage("description must be a string"); +const makeContentValidator = () => + body("content") + .exists() + .withMessage("content is required") + .bail() + .isString() + .withMessage("content must be a string"); const makeGuidlinesValidator = () => body("guidelines") .exists() @@ -56,6 +63,7 @@ const makeImageURIValidator = () => export const createEventDetails = [ makeNameValidator(), makeDescriptionValidator(), + makeContentValidator(), makeGuidlinesValidator(), makeDateValidator(), makeLocationValidator(), diff --git a/frontend/src/api/eventDetails.ts b/frontend/src/api/eventDetails.ts index d354d35c..faf91245 100644 --- a/frontend/src/api/eventDetails.ts +++ b/frontend/src/api/eventDetails.ts @@ -1,4 +1,4 @@ -import { get, handleAPIError, post, put } from "./requests"; +import { del, get, handleAPIError, post, put } from "./requests"; import type { APIResult } from "./requests"; @@ -6,6 +6,7 @@ export type EventDetails = { _id: string; name: string; description: string; + content: string; guidelines: string; date: string; location: string; @@ -32,9 +33,10 @@ export async function getAllEventDetails(): Promise> { } } -type CreateEventDetailsRequest = { +export type CreateEventDetailsRequest = { name: string; description: string; + content: string; guidelines: string; date: string; location: string; @@ -49,14 +51,16 @@ export async function createEventDetails( const json = (await response.json()) as EventDetails; return { success: true, data: json }; } catch (error) { + console.log(error); return handleAPIError(error); } } -type UpdateEventDetailsRequest = { +export type UpdateEventDetailsRequest = { _id: string; name: string; description: string; + content: string; guidelines: string; date: string; location: string; @@ -77,3 +81,13 @@ export async function updateEventDetails( return handleAPIError(error); } } + +export async function deleteEventDetails(id: string): Promise> { + try { + const response = await del(`/api/eventDetails/${id}`); + const json = (await response.json()) as EventDetails; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} diff --git a/frontend/src/app/admin/event-creator/page.module.css b/frontend/src/app/admin/event-creator/page.module.css new file mode 100644 index 00000000..a76ae236 --- /dev/null +++ b/frontend/src/app/admin/event-creator/page.module.css @@ -0,0 +1,68 @@ +@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wdth,wght@0,75..100,300..800;1,75..100,300..800&family=Roboto+Slab:wght@100..900&display=swap"); + +.page { + display: flex; + justify-content: flex-start; + padding-left: 282px; + padding-top: 22px; + padding-bottom: 50px; +} + +.Headings { + text-align: left; + font: var(--font-body); + font-size: 16px; + font-style: normal; + font-weight: 700; + line-height: 24px; /* 133.333% */ + color: white; +} + +.cellentry { + text-align: left; + font: var(--font-body); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 133.333% */ + color: black; +} + +.headingBackground { + background-color: #694c97; /* Replace with your desired color */ +} + +.cellBorderStyle { + border-right: 1px solid #c9c9c9; /* Adjust the border style as needed */ +} + +.selectedRow { + border-radius: 5px; + box-shadow: inset 0 0 0 2px #bda7e0; + box-shadow: inset 0 0.5px 0 2px #bda7e0; +} +.selectedCol { + background: rgba(105, 76, 151, 0.05); +} + +.evenRow { + background-color: #ffffff; /* White color for even rows */ +} + +.oddRow { + background-color: #f8f5fb; /* #F8F5FB color for odd rows */ +} + +.sidebar-container { + position: fixed; + top: 0; + right: 0; + bottom: 0; + width: 400px; /* Adjust width as needed */ + transition: transform 0.3s ease-out; + transform: translateX(100%); +} + +.sidebar-container.open { + transform: translateX(0%); +} diff --git a/frontend/src/app/admin/event-creator/page.tsx b/frontend/src/app/admin/event-creator/page.tsx new file mode 100644 index 00000000..cb280682 --- /dev/null +++ b/frontend/src/app/admin/event-creator/page.tsx @@ -0,0 +1,389 @@ +"use client"; +import Box from "@mui/material/Box"; +import { + DataGrid, + GridColDef, + GridEventListener, + GridRowClassNameParams, + GridRowId, +} from "@mui/x-data-grid"; +import Image from "next/image"; +import React, { useEffect, useState } from "react"; + +import styles from "./page.module.css"; + +import { + CreateEventDetailsRequest, + EventDetails, + createEventDetails, + getAllEventDetails, + getEventDetails, + updateEventDetails, +} from "@/api/eventDetails"; +import EventSidebar from "@/components/EventSidebar"; +import PageToggle from "@/components/PageToggle"; + +export default function EventCreator() { + const columns: GridColDef<(typeof rows)[number]>[] = [ + { + field: "name", + headerName: "Event Title", + width: 372.29, + editable: false, + resizable: false, + headerClassName: `${styles.headingBackground} ${styles.cellBorderStyle} ${styles.Headings}`, + cellClassName: `${styles.cellEntry} ${styles.cellBorderStyle}`, + disableColumnMenu: true, + renderHeader: () =>
Event Title
, + }, + { + field: "description", + headerName: "Subtitle", + width: 372.29, + editable: false, + resizable: false, + headerClassName: `${styles.Headings} ${styles.headingBackground} ${styles.cellBorderStyle}`, + cellClassName: `${styles.cellEntry} ${styles.cellBorderStyle}`, + disableColumnMenu: true, + renderHeader: () =>
Subtitle
, + }, + + { + field: "date", + headerName: "Date", + width: 372.29, + editable: false, + resizable: false, + headerClassName: `${styles.Headings} ${styles.headingBackground} ${styles.cellBorderStyle}`, + cellClassName: `${styles.cellEntry} ${styles.cellBorderStyle}`, + disableColumnMenu: true, + renderHeader: () =>
Date
, + }, + ]; + + const [rows, setRow] = useState([]); + const [rowsCurrent, setRowsCurrent] = React.useState(rows); + const [currentEvents, setCurrentEvents] = useState([]); + const [pastEvents, setPastEvents] = useState([]); + const [pageToggle, setPageToggle] = useState(0); + const [selectedRow, setSelectedRow] = useState(null); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(Math.ceil(rows.length / 14)); + const [selectedEvent, setSelectedEvent] = useState(null); + const [sidebarOpen, setSidebarOpen] = useState(false); + const [rerenderKey, setRerenderKey] = useState(0); + + useEffect(() => { + getAllEventDetails() + .then((result) => { + if (result.success) { + const currentYear = new Date().getFullYear(); + + const filteredCurrent = result.data.filter((item) => { + return true; + // to implement + }); + + const formattedCurrentRows = filteredCurrent.map((item) => ({ + ...item, + id: item._id.toString(), + })); + + setCurrentEvents(formattedCurrentRows); + + const filteredPast = result.data.filter((item) => { + // to implement + }); + + const formattedPastRows = filteredPast.map((item) => ({ + ...item, + id: item._id.toString(), + })); + + setPastEvents(formattedPastRows); + + setRow(formattedCurrentRows); + setRowsCurrent(formattedCurrentRows); + } else { + console.error("ERROR:", result.error); + } + }) + .catch((error) => { + alert(error); + }); + }, []); + + useEffect(() => { + if (selectedRow) { + getEventDetails(selectedRow?.toString()) + .then((result) => { + if (result.success) { + setSelectedEvent(result.data); + setRerenderKey((prevKey) => prevKey + 1); + } else { + console.error("ERROR:", result.error); + } + }) + .catch((error) => { + alert(error); + }); + } else { + setSelectedEvent(null); + } + }, [selectedRow]); + + useEffect(() => { + if (sidebarOpen) { + setRerenderKey((prevKey) => prevKey + 1); + } + }, [sidebarOpen]); + + const handleTogglePage = (index: number) => { + if (index === 0) { + setRowsCurrent(currentEvents); + } else if (index === 1) { + setRowsCurrent(pastEvents); + } + setPageToggle(index); + }; + + const openEvent = (createNew: boolean) => { + if (createNew) { + setSelectedRow(null); + } + setSidebarOpen(true); + }; + + useEffect(() => { + // Update total pages when rows change + setTotalPages(Math.ceil(rows.length / 14)); + }, [rows]); + + const handleCellClick: GridEventListener<"rowClick"> = (params) => { + if (!sidebarOpen) { + setSelectedRow(params.id === selectedRow ? null : params.id); + openEvent(false); + } + }; + + const handleSetSidebarOpen = (open: boolean) => { + setSidebarOpen(open); + }; + const handleUpdateEvent = (eventData: EventDetails) => { + updateEventDetails(eventData) + .then((result) => { + if (!result.success) { + alert(result.error); + console.error("ERROR:", result.error); + } + }) + .catch((error) => { + alert(error); + }); + }; + + const handleCreateEvent = (eventData: CreateEventDetailsRequest) => { + createEventDetails(eventData) + .then((result) => { + if (!result.success) { + alert(result.error); + console.error("ERROR:", result.error); + } + }) + .catch((error) => { + alert(error); + }); + }; + + const handlePreviousPage = () => { + if (currentPage > 1) { + setCurrentPage(currentPage - 1); + } + }; + + const handleNextPage = () => { + if (currentPage < totalPages) { + setCurrentPage(currentPage + 1); + } + }; + + const getRowClassName = (params: GridRowClassNameParams) => { + let rowClasses = ""; + + // Add alternating row colors + rowClasses += params.indexRelativeToCurrentPage % 2 === 0 ? styles.evenRow : styles.oddRow; + + // Add border to the selected row + if (selectedRow === params.id) { + rowClasses += ` ${styles.selectedRow}`; + } + return rowClasses; + }; + + return ( +
+ + {sidebarOpen && ( +
+ +
+ )} + + + + + + + + + + + + + +
+ Previous page + Page +
+ {currentPage} +
+ of + {totalPages} + Next page +
+
+
+
+ ); +} diff --git a/frontend/src/components/EventSidebar.module.css b/frontend/src/components/EventSidebar.module.css new file mode 100644 index 00000000..5383cf52 --- /dev/null +++ b/frontend/src/components/EventSidebar.module.css @@ -0,0 +1,191 @@ +.sidebar { + position: fixed; + top: 0; + bottom: 0; + right: 0; + overflow-y: scroll; + overflow-x: hidden; + overscroll-behavior: contain; + width: 38vw; + z-index: 3; + border-left: 1px solid #c9c9c9; + background: #fff; +} + +.grayOut { + opacity: 0.3; + background: #484848; + position: fixed; + top: 0; + bottom: 0; + right: 0; + width: 38vw; +} + +.closeWindow { + display: flex; + width: 524px; + height: 41px; + padding: 10px 20px; + align-items: center; + gap: 8px; + flex-shrink: 0; + color: var(--Neutral-Gray4, #909090); + border-bottom: 1px solid #c9c9c9; + font: var(--font-body); + font-size: 14px; +} + +.closeWindow:hover { + cursor: pointer; +} + +.sidebarContents { + padding: 60px; + padding-left: 35px; +} + +.sidebarContents h1 { + font: var(--font-small-subtitle); + font-size: 18px; + font-style: normal; + font-weight: 700; + line-height: 150%; /* 27px */ + letter-spacing: 0.36px; +} + +.sidebarContents h2 { + padding-top: 24px; + font: var(--font-body); + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + color: #909090; +} + +.sidebarContents { + font: var(--font-body); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 150% */ +} + +.header { + display: flex; + direction: row; + align-items: center; + justify-content: space-between; + height: 46px; +} + +.sidebar button { + display: flex; + padding: 4px 16px; + justify-content: center; + align-items: center; + gap: 6px; + border-radius: 4px; +} + +.sidebar button img { + padding: 4px; +} + +.sidebar button p { + font: var(--text-body); + font-size: 20px; + font-weight: 700; + line-height: 150%; /* 30px */ + letter-spacing: 0.7px; +} + +.editButton { + background: #694c97; + color: #fff; +} + +.cancelButton { + font: var(--font-body); + background: #fff; + border: 1px solid #694c97; + color: #694c97; +} + +.saveButton { + font: var(--font-body); + background: #694c97; + color: #fff; +} + +.deleteButton { + background: #fff; + border: 1px solid #b93b3b; + color: #b93b3b; +} + +.bottomButtons { + display: flex; + direction: row; + gap: 24px; + justify-content: flex-end; + margin-right: 60px; + margin-bottom: 77px; +} + +.deleteButtonWrapper { + display: flex; + direction: row; + justify-content: center; + margin-top: 50px; +} + +.contentPar { + margin-bottom: 1rem; +} + +.sidebar .alert { + position: relative; + top: 35px; +} + +.sidebar .alert .div { + position: absolute; +} + +.sidebar .alert img { + padding: 0; +} + +.textField { + width: 454px; +} + +.textArea { + width: 454px; + height: 80px; + padding: 12px; + border: 1px solid #d8d8d8; + border-radius: 4px; + font-size: 14px; +} + +.textAreaLong { + width: 454px; + height: 120px; + padding: 12px; + border: 1px solid #d8d8d8; + border-radius: 4px; + font-size: 14px; +} + +.textAreaContent { + width: 100%; + white-space: pre-wrap; + font: var(--font-body); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; +} diff --git a/frontend/src/components/EventSidebar.tsx b/frontend/src/components/EventSidebar.tsx new file mode 100644 index 00000000..2e704653 --- /dev/null +++ b/frontend/src/components/EventSidebar.tsx @@ -0,0 +1,401 @@ +"use client"; +import Image from "next/image"; +import React, { useState } from "react"; + +import { CreateEventDetailsRequest, EventDetails, deleteEventDetails } from "../api/eventDetails"; + +import styles from "./EventSidebar.module.css"; + +import AlertBanner from "@/components/AlertBanner"; +import { TextField } from "@/components/TextField"; +import { WarningModule } from "@/components/WarningModule"; + +type eventSidebarProps = { + eventDetails: null | EventDetails; + setSidebarOpen: (open: boolean) => void; + updateEvent: (eventData: EventDetails) => boolean; + createEvent: (eventData: CreateEventDetailsRequest) => boolean; +}; + +type formErrors = { + name?: boolean; + description?: boolean; + content?: boolean; + guidelines?: boolean; + date?: boolean; + location?: boolean; +}; + +const EventSidebar = ({ + eventDetails, + setSidebarOpen, + updateEvent, + createEvent, +}: eventSidebarProps) => { + const [name, setName] = useState(eventDetails ? eventDetails.name : ""); + const [description, setDescription] = useState(eventDetails ? eventDetails.description : ""); + const [content, setContent] = useState(eventDetails ? eventDetails.content : ""); + const [date, setDate] = useState(eventDetails ? eventDetails.date : ""); + const [location, setLocation] = useState(eventDetails ? eventDetails.location : ""); + const [guidelines, setGuidelines] = useState(eventDetails ? eventDetails.guidelines : ""); + const [isEditing, setIsEditing] = useState(!eventDetails); + const [isDeleting, setIsDeleting] = useState(false); + const [errors, setErrors] = useState({}); + const [warningOpen, setWarningOpen] = useState(false); + const [showAlert, setShowAlert] = useState(false); + + const confirmCancel = () => { + setName(eventDetails ? eventDetails.name : ""); + setDescription(eventDetails ? eventDetails.description : ""); + setContent(eventDetails ? eventDetails.content : ""); + setDate(eventDetails ? eventDetails.date : ""); + setLocation(eventDetails ? eventDetails.location : ""); + setGuidelines(eventDetails ? eventDetails.guidelines : ""); + setIsEditing(false); + setIsDeleting(false); + setErrors({}); + setWarningOpen(false); + setSidebarOpen(false); + }; + + const handleCancel = () => { + if ( + name !== (eventDetails ? eventDetails.name : "") || + description !== (eventDetails ? eventDetails.description : "") || + content !== (eventDetails ? eventDetails.content : "") || + date !== (eventDetails ? eventDetails.date : "") || + location !== (eventDetails ? eventDetails.location : "") || + guidelines !== (eventDetails ? eventDetails.guidelines : "") + ) { + setWarningOpen(true); + } else { + confirmCancel(); + } + }; + + const handleCloseSidebar = () => { + if ( + name !== (eventDetails ? eventDetails.name : "") || + description !== (eventDetails ? eventDetails.description : "") || + content !== (eventDetails ? eventDetails.content : "") || + date !== (eventDetails ? eventDetails.date : "") || + location !== (eventDetails ? eventDetails.location : "") || + guidelines !== (eventDetails ? eventDetails.guidelines : "") + ) { + setWarningOpen(true); + } else { + confirmCancel(); + setSidebarOpen(false); + } + }; + + const handleSave = () => { + setWarningOpen(false); + if ( + name === "" || + description === "" || + content === "" || + date === "" || + location === "" || + guidelines === "" + ) { + setErrors({ + name: name === "", + description: description === "", + content: content === "", + date: date === "", + location: location === "", + guidelines: guidelines === "", + }); + } else { + setIsEditing(false); + if (eventDetails) { + updateEvent({ + _id: eventDetails._id, + name, + description, + content, + guidelines, + date, + location, + imageURI: eventDetails.imageURI, + }); + } else { + createEvent({ + name, + description, + content, + guidelines, + date, + location, + imageURI: "https://tse.ucsd.edu/assets/images/icons__tse-bulb__128.png", + }); + } + setIsEditing(false); + setErrors({}); + setShowAlert(true); + window.location.reload(); + } + }; + + const handleDelete = () => { + setIsDeleting(true); + }; + + const confirmDelete = () => { + if (eventDetails) { + deleteEventDetails(eventDetails._id) + .then((result) => { + if (result.success) { + console.log("successful deletion"); + } else { + console.error("ERROR:", result.error); + } + }) + .catch((error) => { + alert(error); + }); + setSidebarOpen(false); + window.location.reload(); + } + }; + + const alertContent = { + text: "Event Saved!", + }; + + const handleCloseAlert = () => { + setShowAlert(false); + }; + + if (isDeleting) { + return ( +
+
+ {showAlert && } +
+
{ + setSidebarOpen(false); + }} + > + test +

Close Window

+
+
+
+

Event Details

+ + {/* Edit button */} + +
+

Event Title

+

{name}

+

Event Description (short)

+
{description}
+

Event Description (long)

+
{content}
+

Date & Time

+

{date}

+

Location

+

{location}

+

Guidelines

+
{guidelines}
+

Image

+

Placeholder - to be replaced with image

+ + {/* Delete button */} + +
+ +
+
+ +
+
+ ); + } + + if (isEditing) { + return ( +
+ {warningOpen &&
} + {warningOpen && ( + { + setWarningOpen(false); + }} + /> + )} +
{ + handleCloseSidebar(); + }} + > + test +

Close Window

+
+
+
+

Event Details

+
+
+
+ ) => { + setName(event.target.value); + }} + error={errors.name} + /> +

Event Description (short)

+