From 78c766c86af00726accbe5edd0fcf2ad0e4091f9 Mon Sep 17 00:00:00 2001 From: kevindo0720 <80845738+kevindo0720@users.noreply.github.com> Date: Tue, 30 Apr 2024 19:20:32 -0700 Subject: [PATCH 1/6] table ready with backend --- frontend/public/ic_add.svg | 5 + .../admin/newslettercreator/page.module.css | 55 ++++ .../src/app/admin/newslettercreator/page.tsx | 276 ++++++++++++++++++ 3 files changed, 336 insertions(+) create mode 100644 frontend/public/ic_add.svg create mode 100644 frontend/src/app/admin/newslettercreator/page.module.css create mode 100644 frontend/src/app/admin/newslettercreator/page.tsx diff --git a/frontend/public/ic_add.svg b/frontend/public/ic_add.svg new file mode 100644 index 00000000..f96f9e49 --- /dev/null +++ b/frontend/public/ic_add.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/app/admin/newslettercreator/page.module.css b/frontend/src/app/admin/newslettercreator/page.module.css new file mode 100644 index 00000000..2ee3848a --- /dev/null +++ b/frontend/src/app/admin/newslettercreator/page.module.css @@ -0,0 +1,55 @@ +@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 */ +} + diff --git a/frontend/src/app/admin/newslettercreator/page.tsx b/frontend/src/app/admin/newslettercreator/page.tsx new file mode 100644 index 00000000..e7b51b3d --- /dev/null +++ b/frontend/src/app/admin/newslettercreator/page.tsx @@ -0,0 +1,276 @@ +"use client"; +import Box from "@mui/material/Box"; +import { + DataGrid, + GridCellParams, + 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 { + Newsletter, + getAllNewsletters +} from "@/api/newsletter"; +import EmailCopyBtn from "@/components/EmailCopyBtn"; +import RowCopyBtn from "@/components/RowCopyBtn"; +import RowDeleteBtn from "@/components/RowDeleteBtn"; + +export default function MailingList() { + const columns: GridColDef<(typeof rows)[number]>[] = [ + { + field: "title", + headerName: "Newsletter Title", + width: 372.29, + editable: false, + resizable: false, + headerClassName: `${styles.headingBackground} ${styles.cellBorderStyle} ${styles.Headings}`, + cellClassName: `${styles.cellEntry} ${styles.cellBorderStyle}`, + disableColumnMenu: true, + renderHeader: () =>
Newsletter 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 [alertType, setAlertType] = useState(""); + const [hover, setHover] = useState(false); + const [selectedRow, setSelectedRow] = useState(null); + const [currentPage, setCurrentPage] = useState(1); // Track current page + const [totalPages, setTotalPages] = useState(Math.ceil(rows.length / 14)); // Calculate total pages + const [showAlert, setShowAlert] = useState(false); + const [deletedRow, setDeletedRow] = useState(null); + + + useEffect(() => { + getAllNewsletters() + .then((result) => { + if (result.success) { + console.log("Data:", result.data); + + const formattedRows = result.data.map((item) => ({ + ...item, + id: item._id.toString(), + })); + + setRow(formattedRows); + setRowsCurrent(formattedRows); + } else { + console.error("ERROR:", result.error); + } + }) + .catch((error) => { + alert(error); + }); + }, []); + + + + useEffect(() => { + // Update total pages when rows change + setTotalPages(Math.ceil(rows.length / 14)); + }, [rows]); + + + const handleCellClick: GridEventListener<"rowClick"> = (params) => { + setSelectedRow(params.id === selectedRow ? null : params.id); + }; + + const getCellClassName = (params: GridCellParams) => { + let colClasses = ""; + if (params.colDef.field === "email" && hover) { + colClasses += ` ${styles.selectedCol}`; + if (params.id === 2) { + colClasses += ` ${styles.selectedColStart}`; + } + if (params.id === 14) { + colClasses += ` ${styles.selectedColEnd}`; + } + } + return colClasses; + }; + + + + + 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 ( +
+ + + + + + + + + +
+ Previous page + Page +
+ {currentPage} +
+ of + {totalPages} + Next page +
+
+
+
+ ); +} From 2848a4ccf203ea196a72340c74e81ab3d92fddbb Mon Sep 17 00:00:00 2001 From: kevindo0720 <80845738+kevindo0720@users.noreply.github.com> Date: Thu, 9 May 2024 15:45:46 -0700 Subject: [PATCH 2/6] incorporated sidebar with archive section, implemented delete functionality --- backend/src/controllers/newsletter.ts | 20 +- backend/src/routes/newsletter.ts | 1 + backend/src/validators/newsletter.ts | 19 + .../public/{close_icon.svg => ic_close1.svg} | 0 frontend/public/ic_close2.svg | 6 + frontend/public/ic_doublecaretright.svg | 4 + frontend/public/ic_edit.svg | 7 + frontend/src/api/newsletter.ts | 25 +- .../newslettercreator/archive/page.module.css | 55 +++ .../admin/newslettercreator/archive/page.tsx | 368 ++++++++++++++++++ .../src/app/admin/newslettercreator/page.tsx | 184 ++++++--- frontend/src/components/AlertBanner.tsx | 8 +- .../NewsletterDeleteWarning.module.css | 67 ++++ .../components/NewsletterDeleteWarning.tsx | 38 ++ .../components/NewsletterSidebar.module.css | 156 ++++++++ frontend/src/components/NewsletterSidebar.tsx | 359 +++++++++++++++++ .../NewsletterSidebarWarning.module.css | 69 ++++ .../components/NewsletterSidebarWarning.tsx | 38 ++ frontend/src/components/PageToggle.module.css | 26 ++ frontend/src/components/PageToggle.tsx | 31 ++ frontend/src/components/TextField.module.css | 32 ++ frontend/src/components/TextField.tsx | 41 ++ 22 files changed, 1501 insertions(+), 53 deletions(-) rename frontend/public/{close_icon.svg => ic_close1.svg} (100%) create mode 100644 frontend/public/ic_close2.svg create mode 100644 frontend/public/ic_doublecaretright.svg create mode 100644 frontend/public/ic_edit.svg create mode 100644 frontend/src/app/admin/newslettercreator/archive/page.module.css create mode 100644 frontend/src/app/admin/newslettercreator/archive/page.tsx create mode 100644 frontend/src/components/NewsletterDeleteWarning.module.css create mode 100644 frontend/src/components/NewsletterDeleteWarning.tsx create mode 100644 frontend/src/components/NewsletterSidebar.module.css create mode 100644 frontend/src/components/NewsletterSidebar.tsx create mode 100644 frontend/src/components/NewsletterSidebarWarning.module.css create mode 100644 frontend/src/components/NewsletterSidebarWarning.tsx create mode 100644 frontend/src/components/PageToggle.module.css create mode 100644 frontend/src/components/PageToggle.tsx create mode 100644 frontend/src/components/TextField.module.css create mode 100644 frontend/src/components/TextField.tsx diff --git a/backend/src/controllers/newsletter.ts b/backend/src/controllers/newsletter.ts index 5dce921b..e6e2dbea 100644 --- a/backend/src/controllers/newsletter.ts +++ b/backend/src/controllers/newsletter.ts @@ -35,8 +35,9 @@ export const getNewsletter: RequestHandler = async (req, res, next) => { }; export const createNewsletter: RequestHandler = async (req, res, next) => { + console.log(req.body); const errors = validationResult(req); - const { _id, image, title, description, date, content } = req.body; + const { _id, image, title, description, date, content, archive } = req.body; try { validationErrorParser(errors); @@ -48,6 +49,7 @@ export const createNewsletter: RequestHandler = async (req, res, next) => { description, date, content, + archive, }); res.status(201).json(newsletter); @@ -83,3 +85,19 @@ export const updateNewsletter: RequestHandler = async (req, res, next) => { next(error); } }; + +export const deleteNewsletter: RequestHandler = async (req, res, next) => { + const { id } = req.params; + + try { + const newsletter = await Newsletter.findByIdAndDelete(id); + + if (!newsletter) { + throw createHttpError(404, "Newsletter not found."); + } + + res.status(200).json(newsletter); + } catch (error) { + next(error); + } +}; \ No newline at end of file diff --git a/backend/src/routes/newsletter.ts b/backend/src/routes/newsletter.ts index aad2637d..eb5c99a7 100644 --- a/backend/src/routes/newsletter.ts +++ b/backend/src/routes/newsletter.ts @@ -12,5 +12,6 @@ router.put( NewsletterController.updateNewsletter, ); router.post("/", NewsletterValidator.createNewsletter, NewsletterController.createNewsletter); +router.delete("/:id", NewsletterValidator.deleteNewsletter , NewsletterController.deleteNewsletter); export default router; diff --git a/backend/src/validators/newsletter.ts b/backend/src/validators/newsletter.ts index 88af767c..ceb499af 100644 --- a/backend/src/validators/newsletter.ts +++ b/backend/src/validators/newsletter.ts @@ -42,14 +42,33 @@ const makeContentValidator = () => .bail() .isArray() .withMessage("content must be an array of strings"); +const makeArchiveValidator = () => + body("archive") + .exists() + .withMessage("archive is required") + .bail() + .isBoolean() + .withMessage("archive must be a boolean"); export const createNewsletter = [ + makeImageValidator(), + makeTitleValidator(), + makeDescriptionValidator(), + makeDateValidator(), + makeContentValidator(), + makeArchiveValidator(), +]; + +export const updateNewsletter = [ makeIDValidator(), makeImageValidator(), makeTitleValidator(), makeDescriptionValidator(), makeDateValidator(), makeContentValidator(), + makeArchiveValidator(), ]; export const getNewsletter = [makeIDValidator()]; + +export const deleteNewsletter = [makeIDValidator()]; diff --git a/frontend/public/close_icon.svg b/frontend/public/ic_close1.svg similarity index 100% rename from frontend/public/close_icon.svg rename to frontend/public/ic_close1.svg diff --git a/frontend/public/ic_close2.svg b/frontend/public/ic_close2.svg new file mode 100644 index 00000000..dd6d0ef6 --- /dev/null +++ b/frontend/public/ic_close2.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/public/ic_doublecaretright.svg b/frontend/public/ic_doublecaretright.svg new file mode 100644 index 00000000..1e0ad210 --- /dev/null +++ b/frontend/public/ic_doublecaretright.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/public/ic_edit.svg b/frontend/public/ic_edit.svg new file mode 100644 index 00000000..104e5082 --- /dev/null +++ b/frontend/public/ic_edit.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/src/api/newsletter.ts b/frontend/src/api/newsletter.ts index 772144ca..bec3cb15 100644 --- a/frontend/src/api/newsletter.ts +++ b/frontend/src/api/newsletter.ts @@ -1,4 +1,4 @@ -import { get, handleAPIError, post, put } from "./requests"; +import { get, handleAPIError, post, put,del } from "./requests"; import type { APIResult } from "./requests"; @@ -12,6 +12,15 @@ export type Newsletter = { archive: boolean; }; +export type CreateNewsletterRequest = { + image: string; + title: string; + description: string; + date: string; + content: string[]; + archive: boolean; +}; + export async function getNewsletter(id: string): Promise> { try { const response = await get(`/api/newsletter/${id}`); @@ -36,7 +45,9 @@ export async function getAllNewsletters(): Promise> { } } -export async function createNewsletter(newsletter: Newsletter): Promise> { +export async function createNewsletter( + newsletter: CreateNewsletterRequest, +): Promise> { try { const response = await post("/api/newsletter", newsletter); const json = (await response.json()) as Newsletter; @@ -58,3 +69,13 @@ export async function updateNewsletter(newsletter: Newsletter): Promise> { + try { + const response = await del(`/api/newsletter/${id}`); + const json = (await response.json()) as Newsletter; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} \ No newline at end of file diff --git a/frontend/src/app/admin/newslettercreator/archive/page.module.css b/frontend/src/app/admin/newslettercreator/archive/page.module.css new file mode 100644 index 00000000..2ee3848a --- /dev/null +++ b/frontend/src/app/admin/newslettercreator/archive/page.module.css @@ -0,0 +1,55 @@ +@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 */ +} + diff --git a/frontend/src/app/admin/newslettercreator/archive/page.tsx b/frontend/src/app/admin/newslettercreator/archive/page.tsx new file mode 100644 index 00000000..b7efe9d0 --- /dev/null +++ b/frontend/src/app/admin/newslettercreator/archive/page.tsx @@ -0,0 +1,368 @@ +"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 { + CreateNewsletterRequest, + Newsletter, + createNewsletter, + getAllNewsletters, + getNewsletter, + updateNewsletter, + deleteNewsletter, +} from "@/api/newsletter"; +import NewsletterSidebar from "@/components/NewsletterSidebar"; +import PageToggle from "@/components/PageToggle"; + +export default function MailingList() { + const columns: GridColDef<(typeof rows)[number]>[] = [ + { + field: "title", + headerName: "Newsletter Title", + width: 372.29, + editable: false, + resizable: false, + headerClassName: `${styles.headingBackground} ${styles.cellBorderStyle} ${styles.Headings}`, + cellClassName: `${styles.cellEntry} ${styles.cellBorderStyle}`, + disableColumnMenu: true, + renderHeader: () =>
Newsletter 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 [selectedRow, setSelectedRow] = useState(null); + const [currentPage, setCurrentPage] = useState(1); // Track current page + const [totalPages, setTotalPages] = useState(Math.ceil(rows.length / 14)); // Calculate total pages + const [selectedNewsletter, setSelectedNewsletter] = useState(null); + const [sidebarOpen, setSidebarOpen] = useState(false); + const [rerenderKey, setRerenderKey] = useState(0); + + useEffect(() => { + getAllNewsletters() + .then((result) => { + if (result.success) { + const formattedRows = result.data + .filter(item => item.archive) // filter out items where archive is false + .map((item) => ({ + ...item, + id: item._id.toString(), + })); + + setRow(formattedRows); + setRowsCurrent(formattedRows); + } else { + console.error("ERROR:", result.error); + } + }) + .catch((error) => { + alert(error); + }); + }, []); + + useEffect(() => { + if (selectedRow) { + getNewsletter(selectedRow?.toString()) + .then((result) => { + if (result.success) { + setSelectedNewsletter(result.data); + setRerenderKey((prevKey) => prevKey + 1); + } else { + console.error("ERROR:", result.error); + } + }) + .catch((error) => { + alert(error); + }); + } else { + setSelectedNewsletter(null); + } + }, [selectedRow]); + + useEffect(() => { + if (sidebarOpen) { + setRerenderKey((prevKey) => prevKey + 1); + } + }, [sidebarOpen]); + + const openNewsletter = (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); + openNewsletter(false); + } + }; + + + + const handleSetSidebarOpen = (open: boolean) => { + setSidebarOpen(open); + }; + const handleUpdateNewsletter = (newsletterData: Newsletter) => { + updateNewsletter(newsletterData) + .then((result) => { + if (result.success) { + // TODO: add success message, update table + } else { + console.error("ERROR:", result.error); + } + }) + .catch((error) => { + alert(error); + }); + }; + + const handleCreateNewsletter = (newsletterData: CreateNewsletterRequest) => { + console.log(newsletterData); + createNewsletter(newsletterData) + .then((result) => { + if (result.success) { + //TODO: add success message, update table + } else { + 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/app/admin/newslettercreator/page.tsx b/frontend/src/app/admin/newslettercreator/page.tsx index e7b51b3d..ed517660 100644 --- a/frontend/src/app/admin/newslettercreator/page.tsx +++ b/frontend/src/app/admin/newslettercreator/page.tsx @@ -2,7 +2,6 @@ import Box from "@mui/material/Box"; import { DataGrid, - GridCellParams, GridColDef, GridEventListener, GridRowClassNameParams, @@ -13,13 +12,18 @@ import React, { useEffect, useState } from "react"; import styles from "./page.module.css"; + import { + CreateNewsletterRequest, Newsletter, - getAllNewsletters + createNewsletter, + getAllNewsletters, + getNewsletter, + updateNewsletter, + deleteNewsletter, } from "@/api/newsletter"; -import EmailCopyBtn from "@/components/EmailCopyBtn"; -import RowCopyBtn from "@/components/RowCopyBtn"; -import RowDeleteBtn from "@/components/RowDeleteBtn"; +import NewsletterSidebar from "@/components/NewsletterSidebar"; +import PageToggle from "@/components/PageToggle"; export default function MailingList() { const columns: GridColDef<(typeof rows)[number]>[] = [ @@ -60,28 +64,27 @@ export default function MailingList() { ]; + const [rows, setRow] = useState([]); const [rowsCurrent, setRowsCurrent] = React.useState(rows); - const [alertType, setAlertType] = useState(""); - const [hover, setHover] = useState(false); const [selectedRow, setSelectedRow] = useState(null); const [currentPage, setCurrentPage] = useState(1); // Track current page const [totalPages, setTotalPages] = useState(Math.ceil(rows.length / 14)); // Calculate total pages - const [showAlert, setShowAlert] = useState(false); - const [deletedRow, setDeletedRow] = useState(null); - + const [selectedNewsletter, setSelectedNewsletter] = useState(null); + const [sidebarOpen, setSidebarOpen] = useState(false); + const [rerenderKey, setRerenderKey] = useState(0); useEffect(() => { getAllNewsletters() .then((result) => { - if (result.success) { - console.log("Data:", result.data); - - const formattedRows = result.data.map((item) => ({ - ...item, - id: item._id.toString(), - })); - + if (result.success) { + const formattedRows = result.data + .filter(item => !item.archive) // filter out items where archive is false + .map((item) => ({ + ...item, + id: item._id.toString(), + })); + setRow(formattedRows); setRowsCurrent(formattedRows); } else { @@ -93,6 +96,37 @@ export default function MailingList() { }); }, []); + useEffect(() => { + if (selectedRow) { + getNewsletter(selectedRow?.toString()) + .then((result) => { + if (result.success) { + setSelectedNewsletter(result.data); + setRerenderKey((prevKey) => prevKey + 1); + } else { + console.error("ERROR:", result.error); + } + }) + .catch((error) => { + alert(error); + }); + } else { + setSelectedNewsletter(null); + } + }, [selectedRow]); + + useEffect(() => { + if (sidebarOpen) { + setRerenderKey((prevKey) => prevKey + 1); + } + }, [sidebarOpen]); + + const openNewsletter = (createNew: boolean) => { + if (createNew) { + setSelectedRow(null); + } + setSidebarOpen(true); + }; useEffect(() => { @@ -102,21 +136,44 @@ export default function MailingList() { const handleCellClick: GridEventListener<"rowClick"> = (params) => { - setSelectedRow(params.id === selectedRow ? null : params.id); + if (!sidebarOpen) { + setSelectedRow(params.id === selectedRow ? null : params.id); + openNewsletter(false); + } }; - const getCellClassName = (params: GridCellParams) => { - let colClasses = ""; - if (params.colDef.field === "email" && hover) { - colClasses += ` ${styles.selectedCol}`; - if (params.id === 2) { - colClasses += ` ${styles.selectedColStart}`; - } - if (params.id === 14) { - colClasses += ` ${styles.selectedColEnd}`; - } - } - return colClasses; + + + const handleSetSidebarOpen = (open: boolean) => { + setSidebarOpen(open); + }; + const handleUpdateNewsletter = (newsletterData: Newsletter) => { + updateNewsletter(newsletterData) + .then((result) => { + if (result.success) { + // TODO: add success message, update table + } else { + console.error("ERROR:", result.error); + } + }) + .catch((error) => { + alert(error); + }); + }; + + const handleCreateNewsletter = (newsletterData: CreateNewsletterRequest) => { + console.log(newsletterData); + createNewsletter(newsletterData) + .then((result) => { + if (result.success) { + //TODO: add success message, update table + } else { + console.error("ERROR:", result.error); + } + }) + .catch((error) => { + alert(error); + }); }; @@ -150,18 +207,54 @@ export default function MailingList() { return (
- - + + {sidebarOpen && ( + + )} + + + + + + + + + - - void; onClose: () => void; }; @@ -13,7 +13,7 @@ const AlertBanner = ({ text, img, undo, onClose }: ButtonProps) => { return ( ); }; -export default AlertBanner; +export default AlertBanner; \ No newline at end of file diff --git a/frontend/src/components/NewsletterDeleteWarning.module.css b/frontend/src/components/NewsletterDeleteWarning.module.css new file mode 100644 index 00000000..2f90d7d6 --- /dev/null +++ b/frontend/src/components/NewsletterDeleteWarning.module.css @@ -0,0 +1,67 @@ +.wrapper { + position: absolute; + top: 200px; + padding: 24px; + background-color: #fff; + border-radius: 5px; + margin: 52px; + } + + .wrapper button img { + padding: 4px; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + } + + .wrapper button p { + font: var(--text-body); + font-size: 20px; + font-weight: 700; + line-height: 150%; /* 30px */ + letter-spacing: 0.7px; + } + + .closeButton { + position: relative; + float: right; + } + + .saveButton { + background: #694c97; + color: #fff; + } + + .deleteButton { + background: #fff; + border: 1px solid #b93b3b; + color: #b93b3b; + } + + .wrapper h1 { + color: #000; + font: var(--font-small-subtitle); + font-size: 24px; + font-style: normal; + font-weight: 700; + line-height: 150%; /* 36px */ + letter-spacing: 0.48px; + } + + .wrapper p { + color: #000; + font: var(--font-body); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 150% */ + } + + .buttonWrapper { + display: flex; + flex-direction: row; + justify-content: end; + gap: 18px; + margin-top: 18px; + } \ No newline at end of file diff --git a/frontend/src/components/NewsletterDeleteWarning.tsx b/frontend/src/components/NewsletterDeleteWarning.tsx new file mode 100644 index 00000000..6bb4150a --- /dev/null +++ b/frontend/src/components/NewsletterDeleteWarning.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Image from "next/image"; +import React from "react"; + +import styles from "./NewsletterDeleteWarning.module.css"; + +type NewsletterDeleteWarningProps = { + save: () => void; + discard: () => void; + onClose: () => void; +}; + +export const NewsletterDeleteWarning = ({ + save, + discard, + onClose, +}: NewsletterDeleteWarningProps) => { + return ( +
+ +

Are you sure you want to delete this newsletter?

+

This action is permanent and cannot be undone.

+
+ + +
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/NewsletterSidebar.module.css b/frontend/src/components/NewsletterSidebar.module.css new file mode 100644 index 00000000..8180b9da --- /dev/null +++ b/frontend/src/components/NewsletterSidebar.module.css @@ -0,0 +1,156 @@ +.sidebar { + position: fixed; + top: 0; + bottom: 0; + right: 0; + overflow-y: scroll; + overflow-x: hidden; + overscroll-behavior: contain; + width: 36vw; + z-index: 3; + border-left: 1px solid #c9c9c9; + background: #fff; + } + + .grayOut { + opacity: 0.3; + background: #484848; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + + .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 { + background: #fff; + border: 1px solid #694c97; + color: #694c97; + } + + .saveButton { + background: #694c97; + color: #fff; + } + + .deleteButton { + background: #fff; + border: 1px solid #b93b3b; + color: #b93b3b; + } + + .bottomButtons { + display: flex; + direction: row; + gap: 24px; + justify-content: end; + margin-right: 60px; + } + + .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; + } \ No newline at end of file diff --git a/frontend/src/components/NewsletterSidebar.tsx b/frontend/src/components/NewsletterSidebar.tsx new file mode 100644 index 00000000..60d38e71 --- /dev/null +++ b/frontend/src/components/NewsletterSidebar.tsx @@ -0,0 +1,359 @@ +"use client"; +import Image from "next/image"; +import React, { useState } from "react"; +import { useRouter } from 'next/router'; + +import { CreateNewsletterRequest, Newsletter,deleteNewsletter } from "../api/newsletter"; + +import styles from "./NewsletterSidebar.module.css"; + +import AlertBanner from "@/components/AlertBanner"; +import { NewsletterSidebarWarning } from "@/components/NewsletterSidebarWarning"; +import { NewsletterDeleteWarning } from "@/components/NewsletterDeleteWarning"; + +import { TextField } from "@/components/TextField"; + +type newsletterSidebarProps = { + newsletter: null | Newsletter; + setSidebarOpen: (open: boolean) => void; + updateNewsletter: (newsletterData: Newsletter) => void; + createNewsletter: (newsletterData: CreateNewsletterRequest) => void; +}; + +type formErrors = { + title?: boolean; + description?: boolean; + date?: boolean; + content?: boolean; +}; + +const NewsletterSidebar = ({ + newsletter, + setSidebarOpen, + updateNewsletter, + createNewsletter, +}: newsletterSidebarProps) => { + const [title, setTitle] = useState(newsletter ? newsletter.title : ""); + const [description, setDescription] = useState(newsletter ? newsletter.description : ""); + const [date, setDate] = useState(newsletter ? newsletter.date : ""); + const [content, setContent] = useState(newsletter ? newsletter.content : []); + const [isEditing, setIsEditing] = useState(!newsletter); + const [isDeleting, setIsDeleting] = useState(false); + const [errors, setErrors] = useState({}); + const [warningOpen, setWarningOpen] = useState(false); + const [warningDelete, setWarningDelete] = useState(false); + const [showAlert, setShowAlert] = useState(false); + + const confirmCancel = () => { + setTitle(newsletter ? newsletter.title : ""); + setDescription(newsletter ? newsletter.description : ""); + setDate(newsletter ? newsletter.date : ""); + setContent(newsletter ? newsletter.content : []); + setIsEditing(false); + setIsDeleting(false); + setErrors({}); + setWarningOpen(false); + }; + + const handleCancel = () => { + if ( + title !== (newsletter ? newsletter.title : "") || + description !== (newsletter ? newsletter.description : "") || + date !== (newsletter ? newsletter.date : "") || + content !== (newsletter ? newsletter.content : []) + ) { + setWarningOpen(true); + } else { + confirmCancel(); + } + }; + + const handleCloseSidebar = () => { + if ( + title !== (newsletter ? newsletter.title : "") || + description !== (newsletter ? newsletter.description : "") || + date !== (newsletter ? newsletter.date : "") || + content !== (newsletter ? newsletter.content : []) + ) { + setWarningOpen(true); + } else { + confirmCancel(); + setSidebarOpen(false); + } + }; + + const handleSave = () => { + setWarningOpen(false); + if (title === "" || description === "" || date === "" || content.length === 0) { + setErrors({ + title: title === "", + description: description === "", + date: date === "", + content: content.length === 0, + }); + } else { + setIsEditing(false); + if (newsletter) { + updateNewsletter({ + _id: newsletter._id, + image: newsletter.image, + title, + description, + date, + content, + archive: newsletter.archive, + }); + + } else { + createNewsletter({ + image: "/newsletter2.png", + title, + description, + date, + content, + archive: false, + }); + } + setIsEditing(false); + setErrors({}); + setShowAlert(true); + window.location.reload(); + } + }; + + + + const handleDelete = () => { + + setIsDeleting(true); + + }; + + const confirmDelete = () => { + + deleteNewsletter(newsletter._id); + setSidebarOpen(false); + window.location.reload(); + }; + + + const alertContent = { + text: "Newsletter Saved!", + }; + + const handleCloseAlert = () => { + setShowAlert(false); + }; + + + if(isDeleting) { + + return ( +
+ +
+ {showAlert && } +
+
{ + setSidebarOpen(false); + }} + > + test +

Close Window

+
+
+
+

Newsletter Details

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

Newsletter Title

+

{title}

+

Newsletter Description

+

{description}

+

Date & Time

+

{date}

+

Newsletter Cover

+

Placeholder - to be replaced with image

+

Newsletter Content

+ {content.map((paragraph: string, index: number) => ( +

+ {paragraph} +

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

Close Window

+
+
+
+

Newsletter Details

+
+
+
+ ) => { + setTitle(event.target.value); + }} + error={errors.title} + /> + ) => { + setDescription(event.target.value); + }} + error={errors.description} + /> + ) => { + setDate(event.target.value); + }} + error={errors.date} + /> +

Newsletter Cover

+

Placeholder - to be replaced with image

+ ) => { + const contentStr = event.target.value; + setContent(contentStr.split("\n")); + }} + error={errors.content} + /> +
+
+
+
+ {/* Cancel button */} + + {/* Save button */} + +
+
+ ); + + //if is deleting + } + else { + // not in edit mode + return ( +
+ +
+ {showAlert && } +
+
{ + setSidebarOpen(false); + }} + > + test +

Close Window

+
+
+
+

Newsletter Details

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

Newsletter Title

+

{title}

+

Newsletter Description

+

{description}

+

Date & Time

+

{date}

+

Newsletter Cover

+

Placeholder - to be replaced with image

+

Newsletter Content

+ {content.map((paragraph: string, index: number) => ( +

+ {paragraph} +

+ ))} + {/* Delete button */} +
+ +
+
+
+ ); + } +}; + +export default NewsletterSidebar; \ No newline at end of file diff --git a/frontend/src/components/NewsletterSidebarWarning.module.css b/frontend/src/components/NewsletterSidebarWarning.module.css new file mode 100644 index 00000000..905e297f --- /dev/null +++ b/frontend/src/components/NewsletterSidebarWarning.module.css @@ -0,0 +1,69 @@ +.wrapper { + position: absolute; + top: 200px; + padding: 24px; + background-color: #fff; + border-radius: 5px; + margin: 52px; + + + } + + .wrapper button img { + padding: 4px; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + } + + .wrapper button p { + font: var(--text-body); + font-size: 20px; + font-weight: 700; + line-height: 150%; /* 30px */ + letter-spacing: 0.7px; + } + + .closeButton { + position: relative; + float: right; + } + + .saveButton { + background: #694c97; + color: #fff; + } + + .deleteButton { + background: #fff; + border: 1px solid #b93b3b; + color: #b93b3b; + } + + .wrapper h1 { + color: #000; + font: var(--font-small-subtitle); + font-size: 24px; + font-style: normal; + font-weight: 700; + line-height: 150%; /* 36px */ + letter-spacing: 0.48px; + } + + .wrapper p { + color: #000; + font: var(--font-body); + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; /* 150% */ + } + + .buttonWrapper { + display: flex; + flex-direction: row; + justify-content: end; + gap: 18px; + margin-top: 18px; + } \ No newline at end of file diff --git a/frontend/src/components/NewsletterSidebarWarning.tsx b/frontend/src/components/NewsletterSidebarWarning.tsx new file mode 100644 index 00000000..9ec439dd --- /dev/null +++ b/frontend/src/components/NewsletterSidebarWarning.tsx @@ -0,0 +1,38 @@ +"use client"; + +import Image from "next/image"; +import React from "react"; + +import styles from "./NewsletterSidebarWarning.module.css"; + +type NewsletterSidebarWarningProps = { + save: () => void; + discard: () => void; + onClose: () => void; +}; + +export const NewsletterSidebarWarning = ({ + save, + discard, + onClose, +}: NewsletterSidebarWarningProps) => { + return ( +
+ +

You have unsaved changes!

+

Do you want to save the changes you made to this event?

+
+ + +
+
+ ); +}; \ No newline at end of file diff --git a/frontend/src/components/PageToggle.module.css b/frontend/src/components/PageToggle.module.css new file mode 100644 index 00000000..95caff44 --- /dev/null +++ b/frontend/src/components/PageToggle.module.css @@ -0,0 +1,26 @@ +.container { + display: flex; + flex-direction: row; + justify-content: flex-start; + float: left; + margin-top: 12px; + gap: 24px; + margin-bottom: 0px; + padding: 0 12px; + border-bottom: solid 1px #6c6c6c; +} + + .menu { + font: var(--font-body-reg); + font-size: 14px; + color: #6c6c6c; + padding-bottom: 12px; + } + + .menuActive { + font: var(--font-body-bold); + font-size: 14px; + color: var(--color-primary-purple); + border-bottom: solid 3px var(--color-primary-purple); + padding-bottom: 12px; + } \ No newline at end of file diff --git a/frontend/src/components/PageToggle.tsx b/frontend/src/components/PageToggle.tsx new file mode 100644 index 00000000..1027fe9d --- /dev/null +++ b/frontend/src/components/PageToggle.tsx @@ -0,0 +1,31 @@ +import Link from "next/link"; +import React from "react"; + +import styles from "./PageToggle.module.css"; + +type PageToggleProps = { + pages: string[]; + links: string[]; + currPage: number; +}; + +const PageToggle = ({ pages, links, currPage }: PageToggleProps) => { + return ( +
+ {pages.map((page, index) => { + const link = links[index]; + return ( + + {page} + + ); + })} +
+ ); +}; + +export default PageToggle; \ No newline at end of file diff --git a/frontend/src/components/TextField.module.css b/frontend/src/components/TextField.module.css new file mode 100644 index 00000000..3f7e3077 --- /dev/null +++ b/frontend/src/components/TextField.module.css @@ -0,0 +1,32 @@ +.wrapper { + padding-top: 24px; + width: 20rem; + height: auto; + } + + .label { + font: var(--font-body); + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + color: #909090; + } + + .input { + width: 100%; + line-height: 20px; + padding: 6px 12px; + border: 1px solid #d8d8d8; + border-radius: 4px; + font: var(--font-body); + color: var(--Neutral-Black, #000); + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + } + + .input.error { + border-color: #b93b3b; + } \ No newline at end of file diff --git a/frontend/src/components/TextField.tsx b/frontend/src/components/TextField.tsx new file mode 100644 index 00000000..017bb3a8 --- /dev/null +++ b/frontend/src/components/TextField.tsx @@ -0,0 +1,41 @@ +import React from "react"; + +import styles from "./TextField.module.css"; + +/** + * See `src/components/Button.tsx` for basic info about prop interfaces. Here we also use an `Omit` + * type, which is a built-in TypeScript utility type. `Omit` gives us the type X, excluding + * any fields Y. In this case, we are extending `React.ComponentProps<"input">` (the props that an + * `` component can receive), but excluding the specific prop `type`. We exclude `type` + * because we will set `type="text"` on the underlying `` component, so there's no point in + * allowing the developer to pass that prop in themselves. + */ +export type TextFieldProps = { + label: string; + error?: boolean; +} & Omit, "type">; + +/** + * See `src/components/Button.tsx` for an explanation of `React.forwardRef`. + */ +export const TextField = React.forwardRef(function TextField( + { label, error = false, className, ...props }, + ref, +) { + let wrapperClass = styles.wrapper; + if (className) { + wrapperClass += ` ${className}`; + } + let inputClass = styles.input; + if (error) { + inputClass += ` ${styles.error}`; + } + return ( +
+ +
+ ); +}); \ No newline at end of file From 70435cfd5b2c36b6c4a62017901510388c46e44d Mon Sep 17 00:00:00 2001 From: kevindo0720 <80845738+kevindo0720@users.noreply.github.com> Date: Thu, 9 May 2024 18:17:09 -0700 Subject: [PATCH 3/6] create empty popups on archive --- .../admin/newslettercreator/archive/page.tsx | 3 +- frontend/src/components/NewsletterSidebar.tsx | 4 + .../components/NewsletterSidebarArchive.tsx | 363 ++++++++++++++++++ 3 files changed, 368 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/NewsletterSidebarArchive.tsx diff --git a/frontend/src/app/admin/newslettercreator/archive/page.tsx b/frontend/src/app/admin/newslettercreator/archive/page.tsx index b7efe9d0..23a677b7 100644 --- a/frontend/src/app/admin/newslettercreator/archive/page.tsx +++ b/frontend/src/app/admin/newslettercreator/archive/page.tsx @@ -22,7 +22,7 @@ import { updateNewsletter, deleteNewsletter, } from "@/api/newsletter"; -import NewsletterSidebar from "@/components/NewsletterSidebar"; +import NewsletterSidebar from "@/components/NewsletterSidebarArchive"; import PageToggle from "@/components/PageToggle"; export default function MailingList() { @@ -204,7 +204,6 @@ export default function MailingList() { return rowClasses; }; - return (
diff --git a/frontend/src/components/NewsletterSidebar.tsx b/frontend/src/components/NewsletterSidebar.tsx index 60d38e71..ec5af7fd 100644 --- a/frontend/src/components/NewsletterSidebar.tsx +++ b/frontend/src/components/NewsletterSidebar.tsx @@ -81,6 +81,7 @@ const NewsletterSidebar = ({ setSidebarOpen(false); } }; + const handleSave = () => { setWarningOpen(false); @@ -105,6 +106,8 @@ const NewsletterSidebar = ({ }); } else { + + createNewsletter({ image: "/newsletter2.png", title, @@ -121,6 +124,7 @@ const NewsletterSidebar = ({ } }; + const handleDelete = () => { diff --git a/frontend/src/components/NewsletterSidebarArchive.tsx b/frontend/src/components/NewsletterSidebarArchive.tsx new file mode 100644 index 00000000..7119b4d2 --- /dev/null +++ b/frontend/src/components/NewsletterSidebarArchive.tsx @@ -0,0 +1,363 @@ +"use client"; +import Image from "next/image"; +import React, { useState } from "react"; +import { useRouter } from 'next/router'; + +import { CreateNewsletterRequest, Newsletter,deleteNewsletter } from "../api/newsletter"; + +import styles from "./NewsletterSidebar.module.css"; + +import AlertBanner from "@/components/AlertBanner"; +import { NewsletterSidebarWarning } from "@/components/NewsletterSidebarWarning"; +import { NewsletterDeleteWarning } from "@/components/NewsletterDeleteWarning"; + +import { TextField } from "@/components/TextField"; + +type newsletterSidebarProps = { + newsletter: null | Newsletter; + setSidebarOpen: (open: boolean) => void; + updateNewsletter: (newsletterData: Newsletter) => void; + createNewsletter: (newsletterData: CreateNewsletterRequest) => void; +}; + +type formErrors = { + title?: boolean; + description?: boolean; + date?: boolean; + content?: boolean; +}; + +const NewsletterSidebar = ({ + newsletter, + setSidebarOpen, + updateNewsletter, + createNewsletter, +}: newsletterSidebarProps) => { + const [title, setTitle] = useState(newsletter ? newsletter.title : ""); + const [description, setDescription] = useState(newsletter ? newsletter.description : ""); + const [date, setDate] = useState(newsletter ? newsletter.date : ""); + const [content, setContent] = useState(newsletter ? newsletter.content : []); + const [isEditing, setIsEditing] = useState(!newsletter); + const [isDeleting, setIsDeleting] = useState(false); + const [errors, setErrors] = useState({}); + const [warningOpen, setWarningOpen] = useState(false); + const [warningDelete, setWarningDelete] = useState(false); + const [showAlert, setShowAlert] = useState(false); + + const confirmCancel = () => { + setTitle(newsletter ? newsletter.title : ""); + setDescription(newsletter ? newsletter.description : ""); + setDate(newsletter ? newsletter.date : ""); + setContent(newsletter ? newsletter.content : []); + setIsEditing(false); + setIsDeleting(false); + setErrors({}); + setWarningOpen(false); + }; + + const handleCancel = () => { + if ( + title !== (newsletter ? newsletter.title : "") || + description !== (newsletter ? newsletter.description : "") || + date !== (newsletter ? newsletter.date : "") || + content !== (newsletter ? newsletter.content : []) + ) { + setWarningOpen(true); + } else { + confirmCancel(); + } + }; + + const handleCloseSidebar = () => { + if ( + title !== (newsletter ? newsletter.title : "") || + description !== (newsletter ? newsletter.description : "") || + date !== (newsletter ? newsletter.date : "") || + content !== (newsletter ? newsletter.content : []) + ) { + setWarningOpen(true); + } else { + confirmCancel(); + setSidebarOpen(false); + } + }; + + + const handleSave = () => { + setWarningOpen(false); + if (title === "" || description === "" || date === "" || content.length === 0) { + setErrors({ + title: title === "", + description: description === "", + date: date === "", + content: content.length === 0, + }); + } else { + setIsEditing(false); + if (newsletter) { + updateNewsletter({ + _id: newsletter._id, + image: newsletter.image, + title, + description, + date, + content, + archive: newsletter.archive, + }); + + } else { + + + createNewsletter({ + image: "/newsletter2.png", + title, + description, + date, + content, + archive: true, + }); + } + setIsEditing(false); + setErrors({}); + setShowAlert(true); + window.location.reload(); + } + }; + + + + + const handleDelete = () => { + + setIsDeleting(true); + + }; + + const confirmDelete = () => { + + deleteNewsletter(newsletter._id); + setSidebarOpen(false); + window.location.reload(); + }; + + + const alertContent = { + text: "Newsletter Saved!", + }; + + const handleCloseAlert = () => { + setShowAlert(false); + }; + + + if(isDeleting) { + + return ( +
+ +
+ {showAlert && } +
+
{ + setSidebarOpen(false); + }} + > + test +

Close Window

+
+
+
+

Newsletter Details

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

Newsletter Title

+

{title}

+

Newsletter Description

+

{description}

+

Date & Time

+

{date}

+

Newsletter Cover

+

Placeholder - to be replaced with image

+

Newsletter Content

+ {content.map((paragraph: string, index: number) => ( +

+ {paragraph} +

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

Close Window

+
+
+
+

Newsletter Details

+
+
+
+ ) => { + setTitle(event.target.value); + }} + error={errors.title} + /> + ) => { + setDescription(event.target.value); + }} + error={errors.description} + /> + ) => { + setDate(event.target.value); + }} + error={errors.date} + /> +

Newsletter Cover

+

Placeholder - to be replaced with image

+ ) => { + const contentStr = event.target.value; + setContent(contentStr.split("\n")); + }} + error={errors.content} + /> +
+
+
+
+ {/* Cancel button */} + + {/* Save button */} + +
+
+ ); + + //if is deleting + } + else { + // not in edit mode + return ( +
+ +
+ {showAlert && } +
+
{ + setSidebarOpen(false); + }} + > + test +

Close Window

+
+
+
+

Newsletter Details

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

Newsletter Title

+

{title}

+

Newsletter Description

+

{description}

+

Date & Time

+

{date}

+

Newsletter Cover

+

Placeholder - to be replaced with image

+

Newsletter Content

+ {content.map((paragraph: string, index: number) => ( +

+ {paragraph} +

+ ))} + {/* Delete button */} +
+ +
+
+
+ ); + } +}; + +export default NewsletterSidebar; \ No newline at end of file From f45cabbefe931faad32012c5ef724edfefd93ba9 Mon Sep 17 00:00:00 2001 From: kevindo0720 <80845738+kevindo0720@users.noreply.github.com> Date: Fri, 10 May 2024 15:57:01 -0700 Subject: [PATCH 4/6] make content input box expandable --- .../src/app/admin/newslettercreator/page.tsx | 2 - frontend/src/components/NewsletterSidebar.tsx | 3 +- .../components/NewsletterSidebarArchive.tsx | 3 +- frontend/src/components/TextField.module.css | 21 +++++++++- frontend/src/components/TextFieldContent.tsx | 41 +++++++++++++++++++ 5 files changed, 64 insertions(+), 6 deletions(-) create mode 100644 frontend/src/components/TextFieldContent.tsx diff --git a/frontend/src/app/admin/newslettercreator/page.tsx b/frontend/src/app/admin/newslettercreator/page.tsx index ed517660..462041ba 100644 --- a/frontend/src/app/admin/newslettercreator/page.tsx +++ b/frontend/src/app/admin/newslettercreator/page.tsx @@ -64,7 +64,6 @@ export default function MailingList() { ]; - const [rows, setRow] = useState([]); const [rowsCurrent, setRowsCurrent] = React.useState(rows); const [selectedRow, setSelectedRow] = useState(null); @@ -143,7 +142,6 @@ export default function MailingList() { }; - const handleSetSidebarOpen = (open: boolean) => { setSidebarOpen(open); }; diff --git a/frontend/src/components/NewsletterSidebar.tsx b/frontend/src/components/NewsletterSidebar.tsx index ec5af7fd..0d62aca7 100644 --- a/frontend/src/components/NewsletterSidebar.tsx +++ b/frontend/src/components/NewsletterSidebar.tsx @@ -12,6 +12,7 @@ import { NewsletterSidebarWarning } from "@/components/NewsletterSidebarWarning" import { NewsletterDeleteWarning } from "@/components/NewsletterDeleteWarning"; import { TextField } from "@/components/TextField"; +import { TextFieldContent } from "@/components/TextFieldContent"; type newsletterSidebarProps = { newsletter: null | Newsletter; @@ -272,7 +273,7 @@ const NewsletterSidebar = ({ />

Newsletter Cover

Placeholder - to be replaced with image

-

Newsletter Cover

Placeholder - to be replaced with image

- ` gives us the type X, excluding + * any fields Y. In this case, we are extending `React.ComponentProps<"input">` (the props that an + * `` component can receive), but excluding the specific prop `type`. We exclude `type` + * because we will set `type="text"` on the underlying `` component, so there's no point in + * allowing the developer to pass that prop in themselves. + */ +export type TextFieldContentProps = { + label: string; + error?: boolean; +} & Omit, "type">; + +/** + * See `src/components/Button.tsx` for an explanation of `React.forwardRef`. + */ +export const TextFieldContent = React.forwardRef(function TextField( + { label, error = false, className, ...props }, + ref, +) { + let wrapperClass = styles.wrapper; + if (className) { + wrapperClass += ` ${className}`; + } + let inputClass = styles.inputcontent; + if (error) { + inputClass += ` ${styles.error}`; + } + return ( +
+