From 2beff5bbb0da10f9c3d06efebda6a7eaa50525ed Mon Sep 17 00:00:00 2001 From: Sophia Zhu Date: Tue, 9 Apr 2024 15:48:09 -0700 Subject: [PATCH 1/8] add mui table --- frontend/package-lock.json | 109 ++++++++++++------- frontend/package.json | 1 + frontend/src/app/admin/mailing-list/page.tsx | 52 +++++++++ 3 files changed, 123 insertions(+), 39 deletions(-) create mode 100644 frontend/src/app/admin/mailing-list/page.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b04ac53b..36363f6a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,6 +12,7 @@ "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.15.12", "@mui/material": "^5.15.12", + "@mui/x-data-grid": "^7.1.1", "html2canvas": "^1.4.1", "html2pdf.js": "^0.9.3", "jspdf": "^2.5.1", @@ -242,9 +243,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -658,14 +659,14 @@ } }, "node_modules/@mui/base": { - "version": "5.0.0-beta.38", - "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.38.tgz", - "integrity": "sha512-AsjD6Y1X5A1qndxz8xCcR8LDqv31aiwlgWMPxFAX/kCKiIGKlK65yMeVZ62iQr/6LBz+9hSKLiD1i4TZdAHKcQ==", + "version": "5.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz", + "integrity": "sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==", "dependencies": { "@babel/runtime": "^7.23.9", "@floating-ui/react-dom": "^2.0.8", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.12", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", "@popperjs/core": "^2.11.8", "clsx": "^2.1.0", "prop-types": "^15.8.1" @@ -689,9 +690,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "5.15.12", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.12.tgz", - "integrity": "sha512-brRO+tMFLpGyjEYHrX97bzqeF6jZmKpqqe1rY0LyIHAwP6xRVzh++zSecOQorDOCaZJg4XkGT9xfD+RWOWxZBA==", + "version": "5.15.15", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.15.tgz", + "integrity": "sha512-aXnw29OWQ6I5A47iuWEI6qSSUfH6G/aCsW9KmW3LiFqr7uXZBK4Ks+z8G+qeIub8k0T5CMqlT2q0L+ZJTMrqpg==", "funding": { "type": "opencollective", "url": "https://opencollective.com/mui-org" @@ -723,16 +724,16 @@ } }, "node_modules/@mui/material": { - "version": "5.15.12", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.12.tgz", - "integrity": "sha512-vXJGg6KNKucsvbW6l7w9zafnpOp0CWc0Wx4mDykuABTpQ5QQBnZxP7+oB4yAS1hDZQ1WobbeIl0CjxK4EEahkA==", + "version": "5.15.15", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.15.tgz", + "integrity": "sha512-3zvWayJ+E1kzoIsvwyEvkTUKVKt1AjchFFns+JtluHCuvxgKcLSRJTADw37k0doaRtVAsyh8bz9Afqzv+KYrIA==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/base": "5.0.0-beta.38", - "@mui/core-downloads-tracker": "^5.15.12", - "@mui/system": "^5.15.12", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.12", + "@mui/base": "5.0.0-beta.40", + "@mui/core-downloads-tracker": "^5.15.15", + "@mui/system": "^5.15.15", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", "@types/react-transition-group": "^4.4.10", "clsx": "^2.1.0", "csstype": "^3.1.3", @@ -772,12 +773,12 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, "node_modules/@mui/private-theming": { - "version": "5.15.12", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.12.tgz", - "integrity": "sha512-cqoSo9sgA5HE+8vZClbLrq9EkyOnYysooepi5eKaKvJ41lReT2c5wOZAeDDM1+xknrMDos+0mT2zr3sZmUiRRA==", + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.14.tgz", + "integrity": "sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/utils": "^5.15.12", + "@mui/utils": "^5.15.14", "prop-types": "^15.8.1" }, "engines": { @@ -798,9 +799,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "5.15.11", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.11.tgz", - "integrity": "sha512-So21AhAngqo07ces4S/JpX5UaMU2RHXpEA6hNzI6IQjd/1usMPxpgK8wkGgTe3JKmC2KDmH8cvoycq5H3Ii7/w==", + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.14.tgz", + "integrity": "sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==", "dependencies": { "@babel/runtime": "^7.23.9", "@emotion/cache": "^11.11.0", @@ -829,15 +830,15 @@ } }, "node_modules/@mui/system": { - "version": "5.15.12", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.12.tgz", - "integrity": "sha512-/pq+GO6yN3X7r3hAwFTrzkAh7K1bTF5r8IzS79B9eyKJg7v6B/t4/zZYMR6OT9qEPtwf6rYN2Utg1e6Z7F1OgQ==", + "version": "5.15.15", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.15.tgz", + "integrity": "sha512-aulox6N1dnu5PABsfxVGOZffDVmlxPOVgj56HrUnJE8MCSh8lOvvkd47cebIVQQYAjpwieXQXiDPj5pwM40jTQ==", "dependencies": { "@babel/runtime": "^7.23.9", - "@mui/private-theming": "^5.15.12", - "@mui/styled-engine": "^5.15.11", - "@mui/types": "^7.2.13", - "@mui/utils": "^5.15.12", + "@mui/private-theming": "^5.15.14", + "@mui/styled-engine": "^5.15.14", + "@mui/types": "^7.2.14", + "@mui/utils": "^5.15.14", "clsx": "^2.1.0", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -868,9 +869,9 @@ } }, "node_modules/@mui/types": { - "version": "7.2.13", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz", - "integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==", + "version": "7.2.14", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.14.tgz", + "integrity": "sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==", "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0" }, @@ -881,9 +882,9 @@ } }, "node_modules/@mui/utils": { - "version": "5.15.12", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.12.tgz", - "integrity": "sha512-8SDGCnO2DY9Yy+5bGzu00NZowSDtuyHP4H8gunhHGQoIlhlY2Z3w64wBzAOLpYw/ZhJNzksDTnS/i8qdJvxuow==", + "version": "5.15.14", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.14.tgz", + "integrity": "sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==", "dependencies": { "@babel/runtime": "^7.23.9", "@types/prop-types": "^15.7.11", @@ -912,6 +913,31 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, + "node_modules/@mui/x-data-grid": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.1.1.tgz", + "integrity": "sha512-hNvz927lkAznFdy45QPE7mIZVyQhlqveHmTK9+SD0N1us4sSTij90uUJ/roTNDod0VA9f5GqWmNz+5h8ihpz6Q==", + "dependencies": { + "@babel/runtime": "^7.24.0", + "@mui/system": "^5.15.14", + "@mui/utils": "^5.15.14", + "clsx": "^2.1.0", + "prop-types": "^15.8.1", + "reselect": "^4.1.8" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^5.15.14", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + } + }, "node_modules/@next/env": { "version": "14.0.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.4.tgz", @@ -5383,6 +5409,11 @@ "node": ">= 6" } }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", diff --git a/frontend/package.json b/frontend/package.json index a4175523..f55ae898 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -18,6 +18,7 @@ "@emotion/styled": "^11.11.0", "@mui/icons-material": "^5.15.12", "@mui/material": "^5.15.12", + "@mui/x-data-grid": "^7.1.1", "html2canvas": "^1.4.1", "html2pdf.js": "^0.9.3", "jspdf": "^2.5.1", diff --git a/frontend/src/app/admin/mailing-list/page.tsx b/frontend/src/app/admin/mailing-list/page.tsx new file mode 100644 index 00000000..4e484d17 --- /dev/null +++ b/frontend/src/app/admin/mailing-list/page.tsx @@ -0,0 +1,52 @@ +"use client"; +import React from "react"; +import Box from "@mui/material/Box"; +import { DataGrid, GridColDef } from "@mui/x-data-grid"; + +const columns: GridColDef<(typeof rows)[number]>[] = [ + { + field: "lastName", + headerName: "Last name", + width: 150, + editable: true, + }, + { + field: "firstName", + headerName: "First name", + width: 150, + editable: true, + }, +]; + +const rows = [ + { id: 1, lastName: "Snow", firstName: "Jon" }, + { id: 2, lastName: "Lannister", firstName: "Cersei" }, + { id: 3, lastName: "Lannister", firstName: "Jaime" }, + { id: 4, lastName: "Stark", firstName: "Arya" }, + { id: 5, lastName: "Targaryen", firstName: "Daenerys" }, + { id: 6, lastName: "Melisandre", firstName: null }, + { id: 7, lastName: "Clifford", firstName: "Ferrara" }, + { id: 8, lastName: "Frances", firstName: "Rossini" }, + { id: 9, lastName: "Roxie", firstName: "Harvey" }, +]; + +export default function MailingList() { + return ( + + + + ); +} From 6210309d544d80194c12dd44306165e2c6b6fc84 Mon Sep 17 00:00:00 2001 From: Sophia Zhu Date: Sat, 20 Apr 2024 00:29:26 -0700 Subject: [PATCH 2/8] add email copy button --- backend/package-lock.json | 10 + backend/package.json | 1 + frontend/public/close_icon.svg | 3 + frontend/public/copy_icon.svg | 10 + frontend/public/copy_icon_light.svg | 11 + frontend/public/ic_caretleft.svg | 3 + frontend/public/ic_caretright.svg | 5 + .../app/admin/mailing-list/page.module.css | 29 ++ frontend/src/app/admin/mailing-list/page.tsx | 318 ++++++++++++++++-- .../src/components/AlertBanner.module.css | 37 ++ frontend/src/components/AlertBanner.tsx | 26 ++ frontend/src/components/EmailCopyBtn.tsx | 15 + 12 files changed, 436 insertions(+), 32 deletions(-) create mode 100644 frontend/public/close_icon.svg create mode 100644 frontend/public/copy_icon.svg create mode 100644 frontend/public/copy_icon_light.svg create mode 100644 frontend/public/ic_caretleft.svg create mode 100644 frontend/public/ic_caretright.svg create mode 100644 frontend/src/app/admin/mailing-list/page.module.css create mode 100644 frontend/src/components/AlertBanner.module.css create mode 100644 frontend/src/components/AlertBanner.tsx create mode 100644 frontend/src/components/EmailCopyBtn.tsx diff --git a/backend/package-lock.json b/backend/package-lock.json index 180b3b75..79826594 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -23,6 +23,7 @@ "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/nodemailer": "^6.4.14", "@typescript-eslint/eslint-plugin": "^6.18.0", "@typescript-eslint/parser": "^6.18.0", "eslint": "^8.56.0", @@ -379,6 +380,15 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.14", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.14.tgz", + "integrity": "sha512-fUWthHO9k9DSdPCSPRqcu6TWhYyxTBg382vlNIttSe9M7XfsT06y0f24KHXtbnijPGGRIcVvdKHTNikOI6qiHA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.11", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", diff --git a/backend/package.json b/backend/package.json index 5cddad28..23f0f4cf 100644 --- a/backend/package.json +++ b/backend/package.json @@ -29,6 +29,7 @@ "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", + "@types/nodemailer": "^6.4.14", "@typescript-eslint/eslint-plugin": "^6.18.0", "@typescript-eslint/parser": "^6.18.0", "eslint": "^8.56.0", diff --git a/frontend/public/close_icon.svg b/frontend/public/close_icon.svg new file mode 100644 index 00000000..d17abbc7 --- /dev/null +++ b/frontend/public/close_icon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/public/copy_icon.svg b/frontend/public/copy_icon.svg new file mode 100644 index 00000000..14ba14c3 --- /dev/null +++ b/frontend/public/copy_icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/copy_icon_light.svg b/frontend/public/copy_icon_light.svg new file mode 100644 index 00000000..4411ef15 --- /dev/null +++ b/frontend/public/copy_icon_light.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/ic_caretleft.svg b/frontend/public/ic_caretleft.svg new file mode 100644 index 00000000..c37ae237 --- /dev/null +++ b/frontend/public/ic_caretleft.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/ic_caretright.svg b/frontend/public/ic_caretright.svg new file mode 100644 index 00000000..633c55ca --- /dev/null +++ b/frontend/public/ic_caretright.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/app/admin/mailing-list/page.module.css b/frontend/src/app/admin/mailing-list/page.module.css new file mode 100644 index 00000000..ef222e12 --- /dev/null +++ b/frontend/src/app/admin/mailing-list/page.module.css @@ -0,0 +1,29 @@ +@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"); + +.Headings { + text-align: left; + font: var(--font-small-subtitle); + font-size: 16px; + font-style: normal; + font-weight: 900; + 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 */ +} diff --git a/frontend/src/app/admin/mailing-list/page.tsx b/frontend/src/app/admin/mailing-list/page.tsx index 4e484d17..7c16f4b7 100644 --- a/frontend/src/app/admin/mailing-list/page.tsx +++ b/frontend/src/app/admin/mailing-list/page.tsx @@ -1,52 +1,306 @@ "use client"; -import React from "react"; import Box from "@mui/material/Box"; import { DataGrid, GridColDef } from "@mui/x-data-grid"; +import Image from "next/image"; +import React, { useEffect, useState } from "react"; -const columns: GridColDef<(typeof rows)[number]>[] = [ - { - field: "lastName", - headerName: "Last name", - width: 150, - editable: true, - }, - { - field: "firstName", - headerName: "First name", - width: 150, - editable: true, - }, -]; - -const rows = [ - { id: 1, lastName: "Snow", firstName: "Jon" }, - { id: 2, lastName: "Lannister", firstName: "Cersei" }, - { id: 3, lastName: "Lannister", firstName: "Jaime" }, - { id: 4, lastName: "Stark", firstName: "Arya" }, - { id: 5, lastName: "Targaryen", firstName: "Daenerys" }, - { id: 6, lastName: "Melisandre", firstName: null }, - { id: 7, lastName: "Clifford", firstName: "Ferrara" }, - { id: 8, lastName: "Frances", firstName: "Rossini" }, - { id: 9, lastName: "Roxie", firstName: "Harvey" }, -]; +import styles from "./page.module.css"; + +import AlertBanner from "@/components/AlertBanner"; +import EmailCopyBtn from "@/components/EmailCopyBtn"; export default function MailingList() { + const columns: GridColDef<(typeof rows)[number]>[] = [ + { + field: "lastName", + headerName: "Last name", + width: 280, + editable: true, + resizable: false, + headerClassName: `${styles.headingBackground} ${styles.cellBorderStyle} ${styles.Headings}`, + cellClassName: `${styles.cellEntry} ${styles.cellBorderStyle}`, + disableColumnMenu: true, + renderHeader: () =>
Last Name
, + }, + { + field: "firstName", + headerName: "First Name", + width: 280, + editable: true, + resizable: false, + headerClassName: `${styles.Headings} ${styles.headingBackground} ${styles.cellBorderStyle}`, + cellClassName: `${styles.cellEntry} ${styles.cellBorderStyle}`, + disableColumnMenu: true, + renderHeader: () =>
First Name
, + }, + + { + field: "memberSince", + headerName: "Member Since", + width: 280, + editable: true, + resizable: false, + headerClassName: `${styles.Headings} ${styles.headingBackground} ${styles.cellBorderStyle}`, + cellClassName: `${styles.cellEntry} ${styles.cellBorderStyle}`, + disableColumnMenu: true, + renderHeader: () =>
Member Since
, + }, + + { + field: "email", + headerName: "Email", + width: 280, + sortable: false, + editable: true, + resizable: false, + flex: 1, + headerClassName: `${styles.Headings} ${styles.headingBackground}`, + cellClassName: styles.cellEntry, + disableColumnMenu: true, + renderHeader: () => ( +
+
Email
+ +
+ ), + }, + ]; + + const rows = [ + { + id: 1, + lastName: "Snow", + firstName: "Jon", + memberSince: "2021-10-10", + email: "tsejenny4flot@gmail.com", + }, + { + id: 2, + lastName: "Lannister", + firstName: "Cersei", + memberSince: "2021-10-10", + email: "tsekev4flot@gmail.com", + }, + { + id: 3, + lastName: "Lannister", + firstName: "Jaime", + memberSince: "2021-10-10", + email: "tsesophia4flot@gmail.com", + }, + { + id: 4, + lastName: "Stark", + firstName: "Arya", + memberSince: "2021-10-10", + email: "tsejen4flot@gmail.com", + }, + { + id: 5, + lastName: "Targaryen", + firstName: "Daenerys", + memberSince: "2021-10-10", + email: "tsekevin4flot@gmail.com", + }, + { + id: 6, + lastName: "Melisandre", + firstName: "bacad", + memberSince: "2021-10-10", + email: "tsesophia4flot@gmail.com", + }, + { + id: 7, + lastName: "Clifford", + firstName: "Ferrara", + memberSince: "2021-10-10", + email: "tseabc4flot@gmail.com", + }, + { + id: 8, + lastName: "Frances", + firstName: "Rossini", + memberSince: "2021-10-10", + email: "tsevaia4flot@gmail.com", + }, + { + id: 9, + lastName: "Roxie", + firstName: "Harvey", + memberSince: "2021-10-10", + email: "tsebcdadf4flot@gmail.com", + }, + { + id: 10, + lastName: "Melisandre", + firstName: "konichiwa", + memberSince: "2021-10-10", + email: "tsesophia4flot@gmail.com", + }, + { + id: 11, + lastName: "Clifford", + firstName: "Ferrara", + memberSince: "2021-10-10", + email: "tseabc4flot@gmail.com", + }, + { + id: 12, + lastName: "Frances", + firstName: "Rossini", + memberSince: "2021-10-10", + email: "tsevaia4flot@gmail.com", + }, + { + id: 13, + lastName: "Roxie", + firstName: "Harvey", + memberSince: "2021-10-10", + email: "tsebcdadf4flot@gmail.com", + }, + { + id: 14, + lastName: "Melisandre", + firstName: "bakakaka", + memberSince: "2021-10-10", + email: "tsesophia4flot@gmail.com", + }, + { + id: 15, + lastName: "Clifford", + firstName: "Ferrara", + memberSince: "2021-10-10", + email: "tseabc4flot@gmail.com", + }, + { + id: 16, + lastName: "Frances", + firstName: "Rossini", + memberSince: "2021-10-10", + email: "tsevaia4flot@gmail.com", + }, + { + id: 17, + lastName: "Roxie", + firstName: "Harvey", + memberSince: "2021-10-10", + email: "tsebcdadf4flot@gmail.com", + }, + ]; + + 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 textToCopy = "This is the text to copy"; + + const handleCopyText = () => { + navigator.clipboard + .writeText(textToCopy) + .then(() => { + setShowAlert(true); + }) + .catch((error) => { + console.error("Error copying text: ", error); + // You can optionally show an error message here + }); + }; + + const handleCloseAlert = () => { + setShowAlert(false); + }; + + useEffect(() => { + // Update total pages when rows change + setTotalPages(Math.ceil(rows.length / 14)); + }, [rows]); + + const handlePreviousPage = () => { + if (currentPage > 1) { + setCurrentPage(currentPage - 1); + } + }; + + const handleNextPage = () => { + if (currentPage < totalPages) { + setCurrentPage(currentPage + 1); + } + }; + return ( - + + {showAlert && ( + + )} + +
+ Previous page + Page +
+ {currentPage} +
+ of + {totalPages} + Next page +
+
); } diff --git a/frontend/src/components/AlertBanner.module.css b/frontend/src/components/AlertBanner.module.css new file mode 100644 index 00000000..6f236450 --- /dev/null +++ b/frontend/src/components/AlertBanner.module.css @@ -0,0 +1,37 @@ +.wrapper { + position: absolute; + left: 50%; + transform: translateX(-50%); + display: flex; + z-index: 1000; + width: 380px; + padding: 16px 18px; + justify-content: space-between; + align-items: center; + border-radius: 8px; + border: 1px solid var(--Functional-Success, #3bb966); + background: #e1f2df; + box-shadow: 0px 5px 10px 0px rgba(0, 0, 0, 0.15); + font-family: "Open Sans"; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; +} + +.alert { + display: flex; + direction: row; + gap: 12px; + align-items: center; +} + +.closeButton { + background: none; + border: none; + cursor: pointer; +} + +.hidden { + display: none; +} diff --git a/frontend/src/components/AlertBanner.tsx b/frontend/src/components/AlertBanner.tsx new file mode 100644 index 00000000..b3c33730 --- /dev/null +++ b/frontend/src/components/AlertBanner.tsx @@ -0,0 +1,26 @@ +import Image from "next/image"; + +import styles from "./AlertBanner.module.css"; + +type ButtonProps = { + text: string; + img: string; + onClose: () => void; +}; + +const AlertBanner = ({ text, img, onClose }: ButtonProps) => { + return ( + + ); +}; +export default AlertBanner; diff --git a/frontend/src/components/EmailCopyBtn.tsx b/frontend/src/components/EmailCopyBtn.tsx new file mode 100644 index 00000000..bf493bd2 --- /dev/null +++ b/frontend/src/components/EmailCopyBtn.tsx @@ -0,0 +1,15 @@ +import Image from "next/image"; + +type ButtonProps = { + onClick?: () => void; +}; + +const EmailCopyBtn = ({ onClick }: ButtonProps) => { + return ( + + ); +}; + +export default EmailCopyBtn; From f6cf749c86a7ddbe60e9cfee83fb2b9aafb63649 Mon Sep 17 00:00:00 2001 From: Sophia Zhu Date: Sat, 20 Apr 2024 00:34:28 -0700 Subject: [PATCH 3/8] implement copying emails --- frontend/src/app/admin/mailing-list/page.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/admin/mailing-list/page.tsx b/frontend/src/app/admin/mailing-list/page.tsx index 7c16f4b7..f46e43e5 100644 --- a/frontend/src/app/admin/mailing-list/page.tsx +++ b/frontend/src/app/admin/mailing-list/page.tsx @@ -192,11 +192,15 @@ export default function MailingList() { const [totalPages, setTotalPages] = useState(Math.ceil(rows.length / 14)); // Calculate total pages const [showAlert, setShowAlert] = useState(false); - const textToCopy = "This is the text to copy"; + const emailsToCopy = () => { + const values = rows.map((row) => row.email); + const copiedText = values.join("\n"); + return copiedText; + }; const handleCopyText = () => { navigator.clipboard - .writeText(textToCopy) + .writeText(emailsToCopy()) .then(() => { setShowAlert(true); }) From df2a8bc2332a3de6689aac66e1a8ce72d218a2d5 Mon Sep 17 00:00:00 2001 From: Sophia Zhu Date: Sat, 20 Apr 2024 01:47:17 -0700 Subject: [PATCH 4/8] add border to selected row & fix type warnings --- .../app/admin/mailing-list/page.module.css | 5 +++ frontend/src/app/admin/mailing-list/page.tsx | 40 ++++++++++++++++--- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/admin/mailing-list/page.module.css b/frontend/src/app/admin/mailing-list/page.module.css index ef222e12..de5bdb92 100644 --- a/frontend/src/app/admin/mailing-list/page.module.css +++ b/frontend/src/app/admin/mailing-list/page.module.css @@ -27,3 +27,8 @@ .cellBorderStyle { border-right: 1px solid #c9c9c9; /* Adjust the border style as needed */ } + +.selectedRow { + border-radius: 5px; + box-shadow: inset 0 0 0 2px #bda7e0; +} diff --git a/frontend/src/app/admin/mailing-list/page.tsx b/frontend/src/app/admin/mailing-list/page.tsx index f46e43e5..5c0b5510 100644 --- a/frontend/src/app/admin/mailing-list/page.tsx +++ b/frontend/src/app/admin/mailing-list/page.tsx @@ -1,6 +1,12 @@ "use client"; import Box from "@mui/material/Box"; -import { DataGrid, GridColDef } from "@mui/x-data-grid"; +import { + DataGrid, + GridColDef, + GridEventListener, + GridRowClassNameParams, + GridRowId, +} from "@mui/x-data-grid"; import Image from "next/image"; import React, { useEffect, useState } from "react"; @@ -15,7 +21,7 @@ export default function MailingList() { field: "lastName", headerName: "Last name", width: 280, - editable: true, + editable: false, resizable: false, headerClassName: `${styles.headingBackground} ${styles.cellBorderStyle} ${styles.Headings}`, cellClassName: `${styles.cellEntry} ${styles.cellBorderStyle}`, @@ -26,7 +32,7 @@ export default function MailingList() { field: "firstName", headerName: "First Name", width: 280, - editable: true, + editable: false, resizable: false, headerClassName: `${styles.Headings} ${styles.headingBackground} ${styles.cellBorderStyle}`, cellClassName: `${styles.cellEntry} ${styles.cellBorderStyle}`, @@ -38,7 +44,7 @@ export default function MailingList() { field: "memberSince", headerName: "Member Since", width: 280, - editable: true, + editable: false, resizable: false, headerClassName: `${styles.Headings} ${styles.headingBackground} ${styles.cellBorderStyle}`, cellClassName: `${styles.cellEntry} ${styles.cellBorderStyle}`, @@ -51,7 +57,7 @@ export default function MailingList() { headerName: "Email", width: 280, sortable: false, - editable: true, + editable: false, resizable: false, flex: 1, headerClassName: `${styles.Headings} ${styles.headingBackground}`, @@ -188,6 +194,27 @@ export default function MailingList() { }, ]; + const [selectedRow, setSelectedRow] = React.useState(null); + + const handleCellClick: GridEventListener<"rowClick"> = ( + params, // GridRowParams + ) => { + setSelectedRow(params.id === selectedRow ? null : params.id); + }; + + const getRowClassName = (params: GridRowClassNameParams) => { + let rowClasses = ""; + + // Add alternating row colors + rowClasses += params.indexRelativeToCurrentPage % 2 === 0 ? "evenRow" : "oddRow"; + + // Add border to the selected row + if (selectedRow === params.id) { + rowClasses += ` ${styles.selectedRow}`; + } + return rowClasses; + }; + 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); @@ -242,6 +269,9 @@ export default function MailingList() { autoHeight rowHeight={48} hideFooter + rowSelectionModel={selectedRow !== null ? [selectedRow] : []} + onCellClick={handleCellClick} + getRowClassName={getRowClassName} initialState={{ pagination: { paginationModel: { From b446db8bbc4f0ddf6355d0ba9b167a301374a953 Mon Sep 17 00:00:00 2001 From: Sophia Zhu Date: Sat, 20 Apr 2024 02:14:50 -0700 Subject: [PATCH 5/8] add row copy and delete buttons --- frontend/public/copy.svg | 5 ++ .../{copy_icon.svg => copy_icon_dark.svg} | 0 frontend/public/trash.svg | 5 ++ frontend/src/app/admin/mailing-list/page.tsx | 49 +++++++++++++++---- frontend/src/components/RowCopyBtn.tsx | 15 ++++++ frontend/src/components/RowDeleteBtn.tsx | 15 ++++++ 6 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 frontend/public/copy.svg rename frontend/public/{copy_icon.svg => copy_icon_dark.svg} (100%) create mode 100644 frontend/public/trash.svg create mode 100644 frontend/src/components/RowCopyBtn.tsx create mode 100644 frontend/src/components/RowDeleteBtn.tsx diff --git a/frontend/public/copy.svg b/frontend/public/copy.svg new file mode 100644 index 00000000..6cb2419b --- /dev/null +++ b/frontend/public/copy.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/copy_icon.svg b/frontend/public/copy_icon_dark.svg similarity index 100% rename from frontend/public/copy_icon.svg rename to frontend/public/copy_icon_dark.svg diff --git a/frontend/public/trash.svg b/frontend/public/trash.svg new file mode 100644 index 00000000..a21ea3af --- /dev/null +++ b/frontend/public/trash.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/app/admin/mailing-list/page.tsx b/frontend/src/app/admin/mailing-list/page.tsx index 5c0b5510..205afc34 100644 --- a/frontend/src/app/admin/mailing-list/page.tsx +++ b/frontend/src/app/admin/mailing-list/page.tsx @@ -14,6 +14,8 @@ import styles from "./page.module.css"; import AlertBanner from "@/components/AlertBanner"; 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]>[] = [ @@ -66,7 +68,7 @@ export default function MailingList() { renderHeader: () => (
Email
- +
), }, @@ -219,15 +221,29 @@ export default function MailingList() { const [totalPages, setTotalPages] = useState(Math.ceil(rows.length / 14)); // Calculate total pages const [showAlert, setShowAlert] = useState(false); - const emailsToCopy = () => { - const values = rows.map((row) => row.email); - const copiedText = values.join("\n"); - return copiedText; + const handleCopyEmails = () => { + const emailsToCopy = () => { + const values = rows.map((row) => row.email); + const copiedText = values.join("\n"); + return copiedText; + }; + navigator.clipboard + .writeText(emailsToCopy()) + .then(() => { + setShowAlert(true); + }) + .catch((error) => { + console.error("Error copying text: ", error); + // You can optionally show an error message here + }); }; - const handleCopyText = () => { + const handleCopyRow = () => { + const rowToCopy = () => { + return "TODO: implement copying the current row selection"; + }; navigator.clipboard - .writeText(emailsToCopy()) + .writeText(rowToCopy()) .then(() => { setShowAlert(true); }) @@ -237,6 +253,10 @@ export default function MailingList() { }); }; + const handleDeleteRow = () => { + console.log("TODO: implement delete row"); + }; + const handleCloseAlert = () => { setShowAlert(false); }; @@ -259,10 +279,21 @@ export default function MailingList() { }; return ( - + {showAlert && ( - + )} + + {selectedRow !== null && } + {selectedRow !== null && } +
+

Insert search bar

+
+
void; +}; + +const RowCopyBtn = ({ onClick }: ButtonProps) => { + return ( + + ); +}; + +export default RowCopyBtn; diff --git a/frontend/src/components/RowDeleteBtn.tsx b/frontend/src/components/RowDeleteBtn.tsx new file mode 100644 index 00000000..71261dab --- /dev/null +++ b/frontend/src/components/RowDeleteBtn.tsx @@ -0,0 +1,15 @@ +import Image from "next/image"; + +type ButtonProps = { + onClick?: () => void; +}; + +const RowDeleteBtn = ({ onClick }: ButtonProps) => { + return ( + + ); +}; + +export default RowDeleteBtn; From 8389dd8f5459340518e225893aa78681e76f3adc Mon Sep 17 00:00:00 2001 From: kevindo0720 <80845738+kevindo0720@users.noreply.github.com> Date: Thu, 25 Apr 2024 16:12:55 -0700 Subject: [PATCH 6/8] finished everything up to delete functionality; delete functionality to be done --- backend/src/app.ts | 3 +- backend/src/controllers/mailinglistentries.ts | 105 ++++++ backend/src/models/mailinglistentries.ts | 14 + backend/src/routes/mailinglistentries.ts | 21 ++ backend/src/validators/mailinglistentries.ts | 52 +++ frontend/public/ic_search.png | Bin 0 -> 486 bytes frontend/public/trash_icon.svg | 5 + frontend/public/trash_icon_dark.svg | 3 + frontend/src/api/mailinglistentries.ts | 72 ++++ frontend/src/api/requests.ts | 11 +- .../app/admin/mailing-list/page.module.css | 13 + frontend/src/app/admin/mailing-list/page.tsx | 354 +++++++++++------- .../src/components/AlertBanner.module.css | 18 +- frontend/src/components/AlertBanner.tsx | 10 +- frontend/src/components/EmailCopyBtn.tsx | 21 +- frontend/src/components/RowDeleteBtn.tsx | 2 +- 16 files changed, 554 insertions(+), 150 deletions(-) create mode 100644 backend/src/controllers/mailinglistentries.ts create mode 100644 backend/src/models/mailinglistentries.ts create mode 100644 backend/src/routes/mailinglistentries.ts create mode 100644 backend/src/validators/mailinglistentries.ts create mode 100644 frontend/public/ic_search.png create mode 100644 frontend/public/trash_icon.svg create mode 100644 frontend/public/trash_icon_dark.svg create mode 100644 frontend/src/api/mailinglistentries.ts diff --git a/backend/src/app.ts b/backend/src/app.ts index bd7aa33c..cc25baf3 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -14,6 +14,7 @@ import volunteerDetailsRoutes from "./routes/volunteerDetails"; import testimonialRoutes from "src/routes/testimonial"; import newsletterRoutes from "src/routes/newsletter"; // Import newsletter routes import emailRoutes from "src/routes/emails"; +import mailinglistentriesRoutes from "src/routes/mailinglistentries"; const app = express(); @@ -35,7 +36,7 @@ app.use( app.use("/api/subscribers", subscriberRoutes); app.use("/api/member", memberRoutes); app.use("/api/BackgroundImage", backgroundImageRoutes); - +app.use("/api/mailinglistentries", mailinglistentriesRoutes); app.use("/api/eventDetails", eventDetailsRoutes); app.use("/api/volunteerDetails", volunteerDetailsRoutes); app.use("/api/testimonial", testimonialRoutes); diff --git a/backend/src/controllers/mailinglistentries.ts b/backend/src/controllers/mailinglistentries.ts new file mode 100644 index 00000000..93bf6136 --- /dev/null +++ b/backend/src/controllers/mailinglistentries.ts @@ -0,0 +1,105 @@ +// mailingListEntriesController.ts +import { RequestHandler } from "express"; +import { validationResult } from "express-validator"; +import createHttpError from "http-errors"; +import MailingListEntries from "src/models/mailinglistentries"; // Assuming this is your Mongoose model for mailing list entries +import validationErrorParser from "src/util/validationErrorParser"; + +export const getAllMailingListEntries: RequestHandler = async (req, res, next) => { + try { + const mailingListEntries = await MailingListEntries.find({}); + + if (!mailingListEntries || mailingListEntries.length === 0) { + + return res.status(200).json({ message: "No mailing list entries found." }); + } + + res.status(200).json(mailingListEntries); + } catch (error) { + + next(error); + } +}; + +export const getMailingListEntry: RequestHandler = async (req, res, next) => { + const { id } = req.params; + + try { + const mailingListEntry = await MailingListEntries.findOne({ id }); + + if (!mailingListEntry) { + throw createHttpError(404, "Mailing list entry not found."); + } + + res.status(200).json(mailingListEntry); + } catch (error) { + next(error); + } +}; + +export const createMailingListEntry: RequestHandler = async (req, res, next) => { + const errors = validationResult(req); + const { id, lastName, firstName, memberSince, email } = req.body; + + try { + validationErrorParser(errors); + + const existingEntry = await MailingListEntries.findOne({ id }); + if (existingEntry) { + throw createHttpError(409, "Mailing list entry with provided id already exists."); + } + + const mailingListEntry = await MailingListEntries.create({ + id, + lastName, + firstName, + memberSince, + email, + }); + + res.status(201).json(mailingListEntry); + } catch (error) { + next(error); + } +}; + +export const updateMailingListEntry: RequestHandler = async (req, res, next) => { + const errors = validationResult(req); + const { id } = req.params; + const { lastName, firstName, memberSince, email } = req.body; + + try { + validationErrorParser(errors); + + const updatedMailingListEntry = await MailingListEntries.findOneAndUpdate( + { id }, + { lastName, firstName, memberSince, email }, + { new: true } // To return the updated document + ); + + if (!updatedMailingListEntry) { + return res.status(404).json({ message: "Mailing list entry not found." }); + } + + res.status(200).json(updatedMailingListEntry); + } catch (error) { + next(error); + } +}; + +export const deleteMailingListEntry: RequestHandler = async (req, res, next) => { + const { id } = req.params; + + try { + const deletedMailingListEntry = await MailingListEntries.findByIdAndDelete(id); + + if (!deletedMailingListEntry) { + return res.status(404).json({ message: "Mailing list entry not found." }); + } + + // Optionally, you can return the deleted entry in the response + res.status(200).json({ message: "Mailing list entry deleted successfully.", deletedEntry: deletedMailingListEntry }); + } catch (error) { + next(error); + } +}; \ No newline at end of file diff --git a/backend/src/models/mailinglistentries.ts b/backend/src/models/mailinglistentries.ts new file mode 100644 index 00000000..f7ef84ab --- /dev/null +++ b/backend/src/models/mailinglistentries.ts @@ -0,0 +1,14 @@ +import { InferSchemaType, Schema, model } from "mongoose"; + +const mailinglistSchema = new Schema({ +_id: { type: String, required: true }, + id: { type: Number, required: true }, + firstName: { type: String, required: true }, + lastName: { type: String, required: true }, + memberSince: { type: String, required: true }, + email: { type: String, required: true }, +}); + +type MailingListEntries = InferSchemaType; + +export default model("MailingListEntries", mailinglistSchema); \ No newline at end of file diff --git a/backend/src/routes/mailinglistentries.ts b/backend/src/routes/mailinglistentries.ts new file mode 100644 index 00000000..75374807 --- /dev/null +++ b/backend/src/routes/mailinglistentries.ts @@ -0,0 +1,21 @@ +import express from "express"; +import * as MailingListController from "src/controllers/mailinglistentries"; +import * as MailingListValidator from "src/validators/mailinglistentries"; + +const router = express.Router(); + +router.get("/", MailingListController.getAllMailingListEntries); +router.get("/:id", MailingListValidator.getMailingListEntryValidator, MailingListController.getMailingListEntry); +router.put( + "/:id", // getNewsletter validator works to just check ID + MailingListValidator.getMailingListEntryValidator, + MailingListController.updateMailingListEntry, +); +router.post("/", MailingListValidator.createMailingListEntryValidator, MailingListController.createMailingListEntry); + +router.delete( + "/:id", + MailingListController.deleteMailingListEntry, + ); + +export default router; \ No newline at end of file diff --git a/backend/src/validators/mailinglistentries.ts b/backend/src/validators/mailinglistentries.ts new file mode 100644 index 00000000..626e1732 --- /dev/null +++ b/backend/src/validators/mailinglistentries.ts @@ -0,0 +1,52 @@ +import { body } from "express-validator"; + +const makeIDValidator = () => + body("id") + .exists() + .withMessage("ID is required") + .bail() + .isNumeric() + .withMessage("ID must be a number"); + +const makeLastNameValidator = () => + body("lastName") + .exists() + .withMessage("Last name is required") + .bail() + .isString() + .withMessage("Last name must be a string"); + +const makeFirstNameValidator = () => + body("firstName") + .exists() + .withMessage("First name is required") + .bail() + .isString() + .withMessage("First name must be a string"); + +const makeMemberSinceValidator = () => + body("memberSince") + .exists() + .withMessage("Member since date is required") + .bail() + .isISO8601() + .toDate() + .withMessage("Member since date must be in ISO 8601 format"); + +const makeEmailValidator = () => + body("email") + .exists() + .withMessage("Email is required") + .bail() + .isEmail() + .withMessage("Email must be a valid email address"); + +export const createMailingListEntryValidator = [ + makeIDValidator(), + makeLastNameValidator(), + makeFirstNameValidator(), + makeMemberSinceValidator(), + makeEmailValidator(), +]; + +export const getMailingListEntryValidator = [makeIDValidator()]; diff --git a/frontend/public/ic_search.png b/frontend/public/ic_search.png new file mode 100644 index 0000000000000000000000000000000000000000..f44cddefb5991fb26eb33730e317492445376b5e GIT binary patch literal 486 zcmV@P)nhrtTCY~CW8e1&{IY+Rs>-vR_8QAmLMeaV}2t5)3D_Cs&3kKL&;JWTLLpMg zvUrtDvL7Q+RuN_lJfquKxX*Ek`bYP|DBX$z-G9XtAl(RbA*1C=5(zuDA)N?xfwbI3 c^29&HH^w%n`pp9PkiH2?qr literal 0 HcmV?d00001 diff --git a/frontend/public/trash_icon.svg b/frontend/public/trash_icon.svg new file mode 100644 index 00000000..a21ea3af --- /dev/null +++ b/frontend/public/trash_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/public/trash_icon_dark.svg b/frontend/public/trash_icon_dark.svg new file mode 100644 index 00000000..dc3cd78e --- /dev/null +++ b/frontend/public/trash_icon_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/api/mailinglistentries.ts b/frontend/src/api/mailinglistentries.ts new file mode 100644 index 00000000..60bb91eb --- /dev/null +++ b/frontend/src/api/mailinglistentries.ts @@ -0,0 +1,72 @@ +import { get, handleAPIError, post, put, deletedEntry } from "./requests"; +import type { APIResult } from "./requests"; + +export type MailingListEntries = { + id: number + email: string; + memberSince : string; + firstName: string; + lastName: string; +}; + +export async function getMailingListEntry(id: number): Promise> { + try { + const response = await get(`/api/mailinglistentries/${id}`); + const json = (await response.json()) as MailingListEntries; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} + +export async function getAllMailingListEntries(): Promise> { + try { + const response = await get("/api/mailinglistentries"); + const json = (await response.json()) as MailingListEntries[]; + const mailinglistentries = []; + for (const entry of json) { + mailinglistentries.push(entry); + } + return { success: true, data: mailinglistentries }; + } catch (error) { + console.log(error); + return handleAPIError(error); + } +} + +export async function createMailingListEntry(entry: MailingListEntries): Promise> { + try { + const response = await post("/api/mailinglistentries", entry); + const json = (await response.json()) as MailingListEntries; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } +} + +export async function updateMailingListEntry(entry: MailingListEntries): Promise> { + try { + const id = entry.id; + const response = await put(`/api/mailinglistentries/${id}`, entry, { + "Content-Type": "application/json", + }); + const json = (await response.json()) as MailingListEntries; + return { success: true, data: json }; + } catch (error) { + return handleAPIError(error); + } + +} + +export async function deleteMailingListEntry(id: number): Promise> { + try { + const response = await deletedEntry(`/api/mailinglistentries/${id}`); + // Check for successful deletion (assuming the server returns a 204 No Content on success) + const json = (await response.json()) as MailingListEntries; + return { success: true, data: json }; + console.log("been 1243 here") + + } catch (error) { + return handleAPIError(error); + } +} \ No newline at end of file diff --git a/frontend/src/api/requests.ts b/frontend/src/api/requests.ts index ec10c54b..12fbc028 100644 --- a/frontend/src/api/requests.ts +++ b/frontend/src/api/requests.ts @@ -1,4 +1,4 @@ -type Method = "GET" | "POST" | "PUT"; +type Method = "GET" | "POST" | "PUT" | "DELETE"; /** * The first part of the backend API URL, which we will automatically prepend to @@ -82,6 +82,14 @@ export async function get(url: string, headers: Record = {}): Pr return response; } + +export async function deletedEntry(url: string, headers: Record = {}): Promise { + const response = await fetchRequest("DELETE", API_BASE_URL + url, undefined, headers); + console.log("been here ahha"); + await assertOk(response); + return response; +} + /** * Sends a POST request to the provided API URL. * @@ -158,3 +166,4 @@ export function handleAPIError(error: unknown): APIError { } return { success: false, error: `Unknown error: ${String(error)}` }; } + diff --git a/frontend/src/app/admin/mailing-list/page.module.css b/frontend/src/app/admin/mailing-list/page.module.css index de5bdb92..09116f28 100644 --- a/frontend/src/app/admin/mailing-list/page.module.css +++ b/frontend/src/app/admin/mailing-list/page.module.css @@ -31,4 +31,17 @@ .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 */ +} \ No newline at end of file diff --git a/frontend/src/app/admin/mailing-list/page.tsx b/frontend/src/app/admin/mailing-list/page.tsx index 205afc34..19f3b91a 100644 --- a/frontend/src/app/admin/mailing-list/page.tsx +++ b/frontend/src/app/admin/mailing-list/page.tsx @@ -5,19 +5,26 @@ import { GridColDef, GridEventListener, GridRowClassNameParams, + GridCellParams, GridRowId, + GridRowModel, } from "@mui/x-data-grid"; import Image from "next/image"; import React, { useEffect, useState } from "react"; - import styles from "./page.module.css"; - import AlertBanner from "@/components/AlertBanner"; import EmailCopyBtn from "@/components/EmailCopyBtn"; import RowCopyBtn from "@/components/RowCopyBtn"; import RowDeleteBtn from "@/components/RowDeleteBtn"; +import TextField from '@mui/material/TextField'; +import Button from '@mui/material/Button'; +import { getAllMailingListEntries,MailingListEntries,deleteMailingListEntry } from '@/api/mailinglistentries'; // Import your MailingListEntry type + + + export default function MailingList() { + const columns: GridColDef<(typeof rows)[number]>[] = [ { field: "lastName", @@ -68,135 +75,65 @@ export default function MailingList() { renderHeader: () => (
Email
- +
), }, ]; - const rows = [ - { - id: 1, - lastName: "Snow", - firstName: "Jon", - memberSince: "2021-10-10", - email: "tsejenny4flot@gmail.com", - }, - { - id: 2, - lastName: "Lannister", - firstName: "Cersei", - memberSince: "2021-10-10", - email: "tsekev4flot@gmail.com", - }, - { - id: 3, - lastName: "Lannister", - firstName: "Jaime", - memberSince: "2021-10-10", - email: "tsesophia4flot@gmail.com", - }, - { - id: 4, - lastName: "Stark", - firstName: "Arya", - memberSince: "2021-10-10", - email: "tsejen4flot@gmail.com", - }, - { - id: 5, - lastName: "Targaryen", - firstName: "Daenerys", - memberSince: "2021-10-10", - email: "tsekevin4flot@gmail.com", - }, - { - id: 6, - lastName: "Melisandre", - firstName: "bacad", - memberSince: "2021-10-10", - email: "tsesophia4flot@gmail.com", - }, - { - id: 7, - lastName: "Clifford", - firstName: "Ferrara", - memberSince: "2021-10-10", - email: "tseabc4flot@gmail.com", - }, - { - id: 8, - lastName: "Frances", - firstName: "Rossini", - memberSince: "2021-10-10", - email: "tsevaia4flot@gmail.com", - }, - { - id: 9, - lastName: "Roxie", - firstName: "Harvey", - memberSince: "2021-10-10", - email: "tsebcdadf4flot@gmail.com", - }, - { - id: 10, - lastName: "Melisandre", - firstName: "konichiwa", - memberSince: "2021-10-10", - email: "tsesophia4flot@gmail.com", - }, - { - id: 11, - lastName: "Clifford", - firstName: "Ferrara", - memberSince: "2021-10-10", - email: "tseabc4flot@gmail.com", - }, - { - id: 12, - lastName: "Frances", - firstName: "Rossini", - memberSince: "2021-10-10", - email: "tsevaia4flot@gmail.com", - }, - { - id: 13, - lastName: "Roxie", - firstName: "Harvey", - memberSince: "2021-10-10", - email: "tsebcdadf4flot@gmail.com", - }, - { - id: 14, - lastName: "Melisandre", - firstName: "bakakaka", - memberSince: "2021-10-10", - email: "tsesophia4flot@gmail.com", - }, - { - id: 15, - lastName: "Clifford", - firstName: "Ferrara", - memberSince: "2021-10-10", - email: "tseabc4flot@gmail.com", - }, - { - id: 16, - lastName: "Frances", - firstName: "Rossini", - memberSince: "2021-10-10", - email: "tsevaia4flot@gmail.com", - }, - { - id: 17, - lastName: "Roxie", - firstName: "Harvey", - memberSince: "2021-10-10", - email: "tsebcdadf4flot@gmail.com", - }, - ]; + const [rows, setRow] = useState([]); + + useEffect(() => { + getAllMailingListEntries() + .then(result => { + if (result.success) { + console.log('Data:', result.data); // Log the data + setRow(result.data); + } else { + console.error('ERROR:', result.error); // Log any errors + } + }); + }, []); + + + const [rowsCurrent, setRowsCurrent] = React.useState(rows); + + const [searchTerm, setSearchTerm] = React.useState(""); + const [filteredRows, setRows] = useState<{ id: number; lastName: string; firstName: string; memberSince: string; email: string; }[]>([]); + + const handleEmailHover = (hovering: boolean) => { + if (hovering) { + setHover(true); + } else { + setHover(false); + } + }; + + const handleSearch = () => { + const searchTerms = searchTerm.toLowerCase().split(' '); + + let filteredRows = rows.filter((row) => { + const firstName = row.firstName.toLowerCase(); + const lastName = row.lastName.toLowerCase(); + + return searchTerms.every((term) => firstName.includes(term) || lastName.includes(term)); + }); + + if (filteredRows.length !== rows.length) { + setRowsCurrent(filteredRows); + } + + + if (searchTerm === "") { + setRowsCurrent(rows); + } + + console.log("filteredRows:", rowsCurrent); + }; + const [alertType, setAlertType] = useState(""); + const [hover, setHover] = useState(false); + const [selectedRow, setSelectedRow] = useState(null); - const [selectedRow, setSelectedRow] = React.useState(null); const handleCellClick: GridEventListener<"rowClick"> = ( params, // GridRowParams @@ -204,12 +141,27 @@ export default function MailingList() { 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 getRowClassName = (params: GridRowClassNameParams) => { let rowClasses = ""; - + // Add alternating row colors - rowClasses += params.indexRelativeToCurrentPage % 2 === 0 ? "evenRow" : "oddRow"; - + rowClasses += params.indexRelativeToCurrentPage % 2 === 0 ? styles.evenRow : styles.oddRow; + // Add border to the selected row if (selectedRow === params.id) { rowClasses += ` ${styles.selectedRow}`; @@ -230,6 +182,7 @@ export default function MailingList() { navigator.clipboard .writeText(emailsToCopy()) .then(() => { + setAlertType("copyEmails"); setShowAlert(true); }) .catch((error) => { @@ -238,13 +191,18 @@ export default function MailingList() { }); }; + const selectedRowContents = rows.find((row) => row.id === selectedRow); + const email = selectedRowContents !== undefined ? selectedRowContents.email : ""; + const handleCopyRow = () => { const rowToCopy = () => { return "TODO: implement copying the current row selection"; }; navigator.clipboard - .writeText(rowToCopy()) + + .writeText(email) .then(() => { + setAlertType("copyRow"); setShowAlert(true); }) .catch((error) => { @@ -254,10 +212,46 @@ export default function MailingList() { }; const handleDeleteRow = () => { - console.log("TODO: implement delete row"); + if (selectedRow !== null) { + const selectedRowData = rows.find((row) => row.id === selectedRow); + if (selectedRowData) { + // Store a copy of the row data before deletion + const deletedRowCopy = { ...selectedRowData }; + + + // Make an API call to delete the row + console.log("selectedRowData:", selectedRowData._id); + deleteMailingListEntry(selectedRowData._id) + .then((response) => { + if (response.success) { + // Remove the deleted row from the local state + setRow((prevRows) => prevRows.filter((row) => row.id !== selectedRow)); + + // Optionally, show an alert/banner indicating successful deletion + setAlertType("deleteRow"); + setShowAlert(true); + } else { + // Handle error response from API + console.error("Error deleting row:", response.error); + // Optionally, show an error message + } + }) + .catch((error) => { + console.error("Error deleting row:", error); + // Optionally, show an error message + }); + } + } + }; + + const handleUndoDelete = () => { + alert("TODO: implement undo delete (need backend to be completed first)"); + setAlertType(""); + setShowAlert(false); }; const handleCloseAlert = () => { + setAlertType(""); setShowAlert(false); }; @@ -278,31 +272,108 @@ export default function MailingList() { } }; + const alertContent = () => { + switch (alertType) { + case "copyEmails": + return { + text: "Emails copied to clipboard", + icon: "/copy_icon_dark.svg", + }; + case "copyRow": + return { text: "Contact copied", icon: "/copy_icon_dark.svg" }; + case "deleteRow": + return { text: "Contact deleted", icon: "/trash_icon_dark.svg", undo: handleUndoDelete }; + default: + alert("Error: Unknown alert type"); + return { text: "", icon: "" }; + } + }; + + + React.useEffect(() => { + handleSearch();}); + return ( {showAlert && ( )} - - {selectedRow !== null && } - {selectedRow !== null && } -
-

Insert search bar

+ +
+ {selectedRow !== null && } + {selectedRow !== null && } +
+ { + setSearchTerm(e.target.value); + console.log("searchTerm:", e.target.value); + // handleSearch(); + }} + style={{ + paddingLeft: '30px', + border: 'none', + flex: '1', + color: '#484848', + fontFamily: 'Open Sans', + fontSize: '16px', + fontStyle: 'normal', + fontWeight: '400', + lineHeight: '24px' + }} // Make room for the image + /> + search icon +
void; onClose: () => void; }; -const AlertBanner = ({ text, img, onClose }: ButtonProps) => { +const AlertBanner = ({ text, img, undo, onClose }: ButtonProps) => { return (