From 678972dc402e5e01957d4b44288f45d540e97223 Mon Sep 17 00:00:00 2001
From: christenx <99223047+christen03@users.noreply.github.com>
Date: Wed, 15 May 2024 14:14:59 -0700
Subject: [PATCH 01/14] images upload
---
frontend/next.config.js | 1 +
frontend/package-lock.json | 45 +++++++++++++++++++
frontend/package.json | 4 +-
.../src/app/admin/page-editor/page.module.css | 1 +
frontend/src/app/admin/page-editor/page.tsx | 2 +
5 files changed, 52 insertions(+), 1 deletion(-)
diff --git a/frontend/next.config.js b/frontend/next.config.js
index 151cdec6..b8f9012d 100644
--- a/frontend/next.config.js
+++ b/frontend/next.config.js
@@ -8,6 +8,7 @@ const nextConfig = {
"i.imgur.com",
"images.unsplash.com",
"plus.unsplash.com",
+ "firebasestorage.googleapis.com"
],
},
};
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 4c9566f0..9b8b0306 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -24,6 +24,8 @@
"nodemailer": "^6.9.11",
"react": "^18",
"react-dom": "^18",
+ "react-dropzone": "^14.2.3",
+ "react-icons": "^5.2.1",
"react-material-symbols": "^4.3.1",
"reactfire": "^4.2.3"
},
@@ -2342,6 +2344,14 @@
"node": ">= 4.5.0"
}
},
+ "node_modules/attr-accept": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
+ "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/autoprefixer": {
"version": "10.4.17",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz",
@@ -3914,6 +3924,17 @@
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-1.3.8.tgz",
"integrity": "sha512-spKHSBQIxxS81N/O21WmuXA2F6wppUCsutpzenOeZzOCCJ5gEfcbqJP983IrpLXzYmXnMUa6J03SubcNPdKrlg=="
},
+ "node_modules/file-selector": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz",
+ "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
"node_modules/fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -6048,6 +6069,30 @@
"react": "^18.2.0"
}
},
+ "node_modules/react-dropzone": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
+ "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==",
+ "dependencies": {
+ "attr-accept": "^2.2.2",
+ "file-selector": "^0.6.0",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">= 10.13"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8 || 18.0.0"
+ }
+ },
+ "node_modules/react-icons": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz",
+ "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index cf88f234..89c79d07 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -19,9 +19,9 @@
"@mui/icons-material": "^5.15.12",
"@mui/material": "^5.15.12",
"@mui/x-data-grid": "^7.1.1",
+ "@paypal/react-paypal-js": "^8.2.0",
"envalid": "^8.0.0",
"firebase": "^10.11.0",
- "@paypal/react-paypal-js": "^8.2.0",
"html2canvas": "^1.4.1",
"html2pdf.js": "^0.9.3",
"jspdf": "^2.5.1",
@@ -30,6 +30,8 @@
"nodemailer": "^6.9.11",
"react": "^18",
"react-dom": "^18",
+ "react-dropzone": "^14.2.3",
+ "react-icons": "^5.2.1",
"react-material-symbols": "^4.3.1",
"reactfire": "^4.2.3"
},
diff --git a/frontend/src/app/admin/page-editor/page.module.css b/frontend/src/app/admin/page-editor/page.module.css
index c6131f68..5bb35341 100644
--- a/frontend/src/app/admin/page-editor/page.module.css
+++ b/frontend/src/app/admin/page-editor/page.module.css
@@ -1,5 +1,6 @@
.page {
display: flex;
+ flex-direction: column;
justify-content: center;
padding-left: 30px;
padding-top: 50px;
diff --git a/frontend/src/app/admin/page-editor/page.tsx b/frontend/src/app/admin/page-editor/page.tsx
index b41478e6..b317ca0c 100644
--- a/frontend/src/app/admin/page-editor/page.tsx
+++ b/frontend/src/app/admin/page-editor/page.tsx
@@ -1,6 +1,7 @@
// Admin Page Editor landing page
import styles from "./page.module.css";
+import ImageDisplay from "@/components/ImageDisplay";
import PageEditorCard from "@/components/PageEditorCard";
export default function Dashboard() {
@@ -38,6 +39,7 @@ export default function Dashboard() {
last_updated="Month XX, XXXX, XX:XX"
/>
+
);
}
From f8720fe1e5df8c72b07cae7077bc15832e2040fc Mon Sep 17 00:00:00 2001
From: christenx <99223047+christen03@users.noreply.github.com>
Date: Wed, 15 May 2024 14:15:09 -0700
Subject: [PATCH 02/14] images upload
---
frontend/src/app/admin/test-image/page.tsx | 11 ++++++
frontend/src/components/ImageDisplay.tsx | 32 ++++++++++++++++
frontend/src/components/ImageDropzone.tsx | 43 ++++++++++++++++++++++
3 files changed, 86 insertions(+)
create mode 100644 frontend/src/app/admin/test-image/page.tsx
create mode 100644 frontend/src/components/ImageDisplay.tsx
create mode 100644 frontend/src/components/ImageDropzone.tsx
diff --git a/frontend/src/app/admin/test-image/page.tsx b/frontend/src/app/admin/test-image/page.tsx
new file mode 100644
index 00000000..222ec1d7
--- /dev/null
+++ b/frontend/src/app/admin/test-image/page.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+
+import ImageDisplay from '@/components/ImageDisplay';
+
+export default function Page() {
+ return (
+
{/* Tailwind classes for centering and padding */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/frontend/src/components/ImageDisplay.tsx b/frontend/src/components/ImageDisplay.tsx
new file mode 100644
index 00000000..45d98cd4
--- /dev/null
+++ b/frontend/src/components/ImageDisplay.tsx
@@ -0,0 +1,32 @@
+"use client"
+
+import Image from "next/image";
+import { useState } from "react";
+
+import ImageDropzone from "./ImageDropzone";
+
+
+export default function ImageDisplay() {
+ const [images, setImages] = useState([]);
+
+ return (
+
+ {images.map((image, index) => (
+
+ ))}
+
+
+
+
+ );
+ }
\ No newline at end of file
diff --git a/frontend/src/components/ImageDropzone.tsx b/frontend/src/components/ImageDropzone.tsx
new file mode 100644
index 00000000..dfe4e24b
--- /dev/null
+++ b/frontend/src/components/ImageDropzone.tsx
@@ -0,0 +1,43 @@
+"use client"
+import { getDownloadURL, ref, uploadBytes } from 'firebase/storage';
+import React, { useEffect, useState } from 'react';
+import Dropzone from 'react-dropzone';
+import {MdFileUpload} from 'react-icons/md';
+import {useStorage} from 'reactfire';
+type FileDropzoneProps = {
+ setImages: (images: string[]) => void;
+}
+
+export default function FileDropzone({ setImages }: FileDropzoneProps) {
+ const storage = useStorage();
+ const [uploading, setUploading] = useState(false);
+ const [fileName, setFileName] = useState("");
+
+ async function uploadImageToFirebase(file: File) {
+ setUploading(true);
+ const storageRef = ref(storage, `uploads/${file.name}`);
+ try {
+ const uploadResult = await uploadBytes(storageRef, file);
+ setFileName(file.name);
+ const downloadURL = await getDownloadURL(uploadResult.ref);
+ setImages((images) => [...images, downloadURL]);
+ } catch (error) {
+ console.error("Error uploading file:", error);
+ } finally {
+ setUploading(false);
+ }
+ }
+
+
+return (
+ uploadImageToFirebase(acceptedFiles[0])}>
+ {({ getRootProps, getInputProps }) => (
+
+
+
{/* Arrow icon */}
+ {uploading ?
Uploading...
:
Upload an Image
}
+
+ )}
+
+);
+}
\ No newline at end of file
From dd0d724a46b8c6c40dd14deba819faf8df29480c Mon Sep 17 00:00:00 2001
From: christenx <99223047+christen03@users.noreply.github.com>
Date: Wed, 22 May 2024 13:33:02 -0700
Subject: [PATCH 03/14] add image upload to pages
---
.../src/app/admin/page-editor/about/page.tsx | 22 ++++++++++---
.../src/app/admin/page-editor/home/page.tsx | 19 ++++++++---
frontend/src/app/admin/page-editor/page.tsx | 1 -
.../src/app/admin/page-editor/team/page.tsx | 8 +++--
frontend/src/components/Collapsable.tsx | 32 +++++++++++++++++--
frontend/src/components/ImageDisplay.tsx | 16 +++++++---
frontend/src/components/ImageDropzone.tsx | 7 ++--
7 files changed, 83 insertions(+), 22 deletions(-)
diff --git a/frontend/src/app/admin/page-editor/about/page.tsx b/frontend/src/app/admin/page-editor/about/page.tsx
index 08345df4..826ce2c5 100644
--- a/frontend/src/app/admin/page-editor/about/page.tsx
+++ b/frontend/src/app/admin/page-editor/about/page.tsx
@@ -8,7 +8,7 @@ import styles from "./page.module.css";
import AlertBanner from "@/components/AlertBanner";
import Button from "@/components/Button";
import CancelButton from "@/components/CancelButton";
-import Collapsable from "@/components/Collapsable";
+import Collapsable, { UploadImageTypes } from "@/components/Collapsable";
import PageToggle from "@/components/PageToggle";
export default function AboutEditor() {
@@ -20,6 +20,9 @@ export default function AboutEditor() {
const [s2Text, setS2Text] = useState("");
const [s3Subtitle, setS3Subtitle] = useState("");
const [s3Text, setS3Text] = useState("");
+ const [missionImages, setMissionImages] = useState([]);
+const [teamImages, setTeamImages] = useState([]);
+const [contactImages, setContactImages] = useState([]);
const [showAlert, setShowAlert] = useState(false);
@@ -50,6 +53,7 @@ export default function AboutEditor() {
/* Handle Fields upon edit */
const handleEdit = (event: React.ChangeEvent) => {
setIsEdited(true);
+ if(event.target){
if (event.target.id === "Page Subtitle: Subtitle") {
setPhSubtitle(event.target.value);
} else if (event.target.id === "Section 1 - Our Mission: Section Title") {
@@ -65,6 +69,7 @@ export default function AboutEditor() {
} else if (event.target.id === "Section 3 - Contact Us: Body Text") {
setS3Text(event.target.value);
}
+ }
};
const handleSave = () => {
@@ -156,21 +161,30 @@ export default function AboutEditor() {
/>
("");
const [s2Subtitle, setS2Subtitle] = useState("");
const [s2Text, setS2Text] = useState("");
+ const [sponsorImages, setSponsorImages] = useState([]); //state that stores the image urls in the page
/* Get page data from MongoDB */
+ //todo: load current image data from mongoDB and store in state
let pageText;
useEffect(() => {
getPageText("Home")
@@ -32,6 +34,7 @@ export default function HomeEditor() {
setS1Text(pageText.pageSections[1].sectionSubtitle ?? "");
setS2Subtitle(pageText.pageSections[2].sectionTitle ?? "");
setS2Text(pageText.pageSections[2].sectionSubtitle ?? "");
+ //setSponsorImages...
console.log("response.data: ", response.data);
} else {
alert(response.error);
@@ -45,6 +48,7 @@ export default function HomeEditor() {
/* Handle Fields upon edit */
const handleEdit = (event: React.ChangeEvent) => {
setIsEdited(true);
+ if(event.target){
if (event.target.id === "Page Header: Subtitle") {
setPhSubtitle(event.target.value);
} else if (event.target.id === "Section 1: Section Title") {
@@ -56,14 +60,16 @@ export default function HomeEditor() {
} else if (event.target.id === "Section 2: Body Text") {
setS2Text(event.target.value);
}
+ }
};
const handleSave = () => {
// Implement save logic
if (isEdited) {
- console.log("Save changes");
+ console.log("Save changes", sponsorImages);
updatePage({
//Pass edited text to MongoDB
+ //TODO: update with image handling
page: "Home",
pageSections: [
{
@@ -77,6 +83,7 @@ export default function HomeEditor() {
sectionTitle: s2Subtitle,
sectionSubtitle: s2Text,
},
+ //secitionTitle: sponsorImages...
],
})
.then((response) => {
@@ -106,6 +113,7 @@ export default function HomeEditor() {
setS1Text(pageText.pageSections[1].sectionSubtitle ?? "");
setS2Subtitle(pageText.pageSections[2].sectionTitle ?? "");
setS2Text(pageText.pageSections[2].sectionSubtitle ?? "");
+ //setSponsorImages (refetch and reset to what it used to be)
} else {
alert(response.error);
}
@@ -123,7 +131,7 @@ export default function HomeEditor() {
@@ -135,9 +143,12 @@ export default function HomeEditor() {
/>
-
);
}
diff --git a/frontend/src/app/admin/page-editor/team/page.tsx b/frontend/src/app/admin/page-editor/team/page.tsx
index b22b9485..289d8564 100644
--- a/frontend/src/app/admin/page-editor/team/page.tsx
+++ b/frontend/src/app/admin/page-editor/team/page.tsx
@@ -5,7 +5,7 @@ import styles from "./page.module.css";
import Button from "@/components/Button";
import CancelButton from "@/components/CancelButton";
-import Collapsable from "@/components/Collapsable";
+import Collapsable, { UploadImageTypes } from "@/components/Collapsable";
import PageToggle from "@/components/PageToggle";
// import PageEditorCard from "@/components/PageEditorCard";
@@ -45,12 +45,14 @@ export default function TeamEditor() {
"Our dedicated team @ 4 Future Leaders of Tomorrow is a non-profit charitable organization committed in preventing and ending homelessness, hunger and disparity in underprivileged communities. Everyone deserves a chance for a better future!. We are reaching out by providing resources in needed communities - whether it be a delicious meal, warm clothing, educational supplies, referrals, toys or even bus passes",
]}
onChange={handleEdit}
+ imageUploadBox={UploadImageTypes.OUR_MISSION}
/>
diff --git a/frontend/src/components/Collapsable.tsx b/frontend/src/components/Collapsable.tsx
index 9fd68ea1..8b0029a0 100644
--- a/frontend/src/components/Collapsable.tsx
+++ b/frontend/src/components/Collapsable.tsx
@@ -1,17 +1,30 @@
"use client";
import Image from "next/image";
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
import styles from "./Collapsable.module.css";
+import ImageDisplay from "./ImageDisplay";
+
+
+export enum UploadImageTypes {
+ SPONSORS = "Sponsors",
+ OUR_MISSION = "Our_Mission",
+ OUR_TEAM = "Our_Team",
+ CONTACT_US = "Contact_Us"
+}
+
type CollapsableProps = {
title: string;
subsection: string[];
textbox: string[];
+ imageUploadBox: UploadImageTypes | undefined;
+ images: string[] | undefined;
+ setImages : (images: string[]) => void;
onChange: (event: React.ChangeEvent
) => void;
};
-const Collapsable = ({ title, subsection, textbox, onChange }: CollapsableProps) => {
+const Collapsable = ({ title, subsection, textbox, onChange, imageUploadBox, images, setImages }: CollapsableProps) => {
const [open, setOpen] = useState(true);
const toggleSection = () => {
@@ -20,12 +33,20 @@ const Collapsable = ({ title, subsection, textbox, onChange }: CollapsableProps)
const handleChange = (event: React.ChangeEvent) => {
// Auto increase height when typing
+ if(event && event.target){
event.target.style.height = "auto";
event.target.style.height = 2 + event.target.scrollHeight + "px";
+ }
// Call onChange function
onChange(event);
};
+ useEffect(() => {
+ if(images){ //mark change in page when image is uploaded
+ handleChange({} as React.ChangeEvent);
+ }
+ }, [images])
+
return (
@@ -57,6 +78,13 @@ const Collapsable = ({ title, subsection, textbox, onChange }: CollapsableProps)
);
})}
+ {imageUploadBox &&
+ }
)}
diff --git a/frontend/src/components/ImageDisplay.tsx b/frontend/src/components/ImageDisplay.tsx
index 45d98cd4..0c096c61 100644
--- a/frontend/src/components/ImageDisplay.tsx
+++ b/frontend/src/components/ImageDisplay.tsx
@@ -3,11 +3,18 @@
import Image from "next/image";
import { useState } from "react";
+import { UploadImageTypes } from "./Collapsable";
import ImageDropzone from "./ImageDropzone";
-export default function ImageDisplay() {
- const [images, setImages] = useState([]);
+type ImageDisplayProps = {
+ type : UploadImageTypes
+ images: string[]
+ setImages : (images: string[]) => void
+}
+
+
+export default function ImageDisplay({type, images, setImages} : ImageDisplayProps) {
return (
@@ -15,7 +22,7 @@ export default function ImageDisplay() {
);
diff --git a/frontend/src/components/ImageDropzone.tsx b/frontend/src/components/ImageDropzone.tsx
index dfe4e24b..c95c5737 100644
--- a/frontend/src/components/ImageDropzone.tsx
+++ b/frontend/src/components/ImageDropzone.tsx
@@ -6,19 +6,18 @@ import {MdFileUpload} from 'react-icons/md';
import {useStorage} from 'reactfire';
type FileDropzoneProps = {
setImages: (images: string[]) => void;
+ type: string;
}
+export default function FileDropzone({ setImages, type }: FileDropzoneProps) {
-export default function FileDropzone({ setImages }: FileDropzoneProps) {
const storage = useStorage();
const [uploading, setUploading] = useState(false);
- const [fileName, setFileName] = useState("");
async function uploadImageToFirebase(file: File) {
setUploading(true);
- const storageRef = ref(storage, `uploads/${file.name}`);
+ const storageRef = ref(storage, `${type}/${file.name}`);
try {
const uploadResult = await uploadBytes(storageRef, file);
- setFileName(file.name);
const downloadURL = await getDownloadURL(uploadResult.ref);
setImages((images) => [...images, downloadURL]);
} catch (error) {
From ba7e699a01561e01b1322f5f15a8caabf4d85103 Mon Sep 17 00:00:00 2001
From: Jack Hansen <47556286+jackavh@users.noreply.github.com>
Date: Wed, 29 May 2024 14:46:46 -0700
Subject: [PATCH 04/14] refactor pageeditor backend to work with images
---
backend/src/controllers/pageeditor.ts | 20 ++++----
backend/src/models/pageeditor.ts | 11 ++++-
backend/src/routes/pageeditor.ts | 8 +--
backend/src/validators/pageeditor.ts | 55 +++++++--------------
frontend/src/api/pageeditor.ts | 70 ++++++++++++---------------
5 files changed, 68 insertions(+), 96 deletions(-)
diff --git a/backend/src/controllers/pageeditor.ts b/backend/src/controllers/pageeditor.ts
index 4d30bdfb..39351a92 100644
--- a/backend/src/controllers/pageeditor.ts
+++ b/backend/src/controllers/pageeditor.ts
@@ -5,16 +5,16 @@ import PageEditor from "src/models/pageeditor";
import validationErrorParser from "src/util/validationErrorParser";
export const getPage: RequestHandler = async (req, res, next) => {
- const { page } = req.params;
+ const { name } = req.params;
try {
- const pageText = await PageEditor.findOne({ page: page });
+ const page = await PageEditor.findOne({ name: name });
- if (!pageText) {
+ if (!page) {
throw createHttpError(404, "Page not found.");
}
- res.status(200).json(pageText);
+ res.status(200).json(page);
} catch (error) {
next(error);
}
@@ -22,22 +22,20 @@ export const getPage: RequestHandler = async (req, res, next) => {
export const updatePageEditor: RequestHandler = async (req, res, next) => {
const errors = validationResult(req);
- const { page } = req.params;
-
- if (page !== req.body.page) {
+ const { name } = req.params;
+ if (name !== req.body.name) {
// If the page in the URL does not match the page in the body, bad request
res.status(400);
}
try {
validationErrorParser(errors);
-
- const pageText = await PageEditor.findOneAndUpdate({ page }, req.body);
- if (pageText === null) {
+ const page = await PageEditor.findOneAndUpdate({ name: name }, { $set: req.body });
+ if (page === null) {
// No page found
res.status(404);
}
- const updatedPage = await PageEditor.findOne({ page });
+ const updatedPage = await PageEditor.findOne({ name: name });
if (updatedPage === null) {
// No page found after updating, something went wrong
res.status(404);
diff --git a/backend/src/models/pageeditor.ts b/backend/src/models/pageeditor.ts
index 7b3085b4..c1e9c0d4 100644
--- a/backend/src/models/pageeditor.ts
+++ b/backend/src/models/pageeditor.ts
@@ -1,8 +1,15 @@
import { InferSchemaType, Schema, model } from "mongoose";
const pageEditorSchema = new Schema({
- page: { type: String, required: true },
- pageSections: [{ type: Schema.Types.Mixed, required: true }],
+ name: { type: String, required: true },
+ isEdited: { type: Boolean, required: true },
+ fields: [
+ {
+ name: { type: String, required: true },
+ type: { type: String, required: true },
+ data: { type: Schema.Types.Mixed, required: true },
+ },
+ ],
});
type PageEditor = InferSchemaType
;
diff --git a/backend/src/routes/pageeditor.ts b/backend/src/routes/pageeditor.ts
index 9f2c8293..da09ad36 100644
--- a/backend/src/routes/pageeditor.ts
+++ b/backend/src/routes/pageeditor.ts
@@ -4,11 +4,7 @@ import * as PageEditorValidator from "src/validators/pageeditor";
const router = express.Router();
-router.get("/:page", PageEditorValidator.getPageEditor, PageEditorController.getPage);
-router.put(
- "/:page", // getPageEditor validator works to just check page
- // PageEditorValidator.getPageEditor,
- PageEditorController.updatePageEditor,
-);
+router.get("/:name", PageEditorValidator.getPageEditor, PageEditorController.getPage);
+router.put("/:name", PageEditorValidator.updatePageEditor, PageEditorController.updatePageEditor);
export default router;
diff --git a/backend/src/validators/pageeditor.ts b/backend/src/validators/pageeditor.ts
index 90047678..db970e49 100644
--- a/backend/src/validators/pageeditor.ts
+++ b/backend/src/validators/pageeditor.ts
@@ -1,47 +1,28 @@
import { body } from "express-validator";
-const makeIDValidator = () =>
- body("_id")
+const makeNameValidator = () =>
+ body("name")
.exists()
- .withMessage("_id is required")
+ .withMessage("name is required")
.bail()
.isString()
- .withMessage("_id must be a number");
-const makePageValidator = () =>
- body("page")
- .exists()
- .withMessage("image is required")
- .bail()
- .isString()
- .withMessage("image must be a string");
-const makeSubtitleValidator = () =>
- body("ph_subtitle")
- .exists()
- .withMessage("subtitle is required")
- .bail()
- .isString()
- .withMessage("subtitle must be a string");
-const makeTitleValidator = () =>
- body("s1_title")
+ .withMessage("name must be a string");
+
+const makeEditedValidator = () =>
+ body("isEdited")
.exists()
- .withMessage("date is required")
+ .withMessage("isEdited is required")
.bail()
- .isString()
- .withMessage("date must be a string");
-const makeTextValidator = () =>
- body("s1_text")
+ .isBoolean()
+ .withMessage("isEdited must be a boolean");
+
+const makeFieldsValidator = () =>
+ body("fields")
.exists()
- .withMessage("content is required")
+ .withMessage("fields is required")
.bail()
- .isString()
- .withMessage("content must be a string");
-
-export const getPageEditor = [makePageValidator()];
+ .isArray()
+ .withMessage("fields must be an array");
-export const createPageEditor = [
- makeIDValidator(),
- makePageValidator(),
- makeSubtitleValidator(),
- makeTitleValidator(),
- makeTextValidator(),
-];
+export const getPageEditor = [makeNameValidator(), makeEditedValidator(), makeFieldsValidator()];
+export const updatePageEditor = [makeNameValidator(), makeEditedValidator(), makeFieldsValidator()];
diff --git a/frontend/src/api/pageeditor.ts b/frontend/src/api/pageeditor.ts
index 7ee6312c..cc87be99 100644
--- a/frontend/src/api/pageeditor.ts
+++ b/frontend/src/api/pageeditor.ts
@@ -2,60 +2,50 @@ import { get, handleAPIError, put } from "./requests";
import type { APIResult } from "./requests";
-export type PageSection = {
- subtitle?: string;
- sectionTitle?: string;
- sectionSubtitle?: string;
+export type Page = {
+ name: string;
+ isEdited: boolean;
+ fields: Field[];
};
-export type PageText = {
- page: string;
- pageSections: PageSection[];
+export type Field = {
+ name: string;
+ type: string;
+ data: TextData | ImageData | GalleryData;
};
-export async function getPageText(page: string): Promise> {
+export type TextData = {
+ text: string;
+};
+
+export type ImageData = {
+ image: string;
+ hasImage: boolean;
+};
+
+export type GalleryData = {
+ images: string[];
+ maxImages: number;
+};
+
+export async function getPageData(name: string): Promise> {
try {
- const response = await get(`/api/pageeditor/${page}`);
- const json = (await response.json()) as PageText;
+ const response = await get(`/api/pageeditor/${name}`);
+ const json = (await response.json()) as Page;
return { success: true, data: json };
} catch (error) {
return handleAPIError(error);
}
}
-export async function updatePage(pageData: PageText): Promise> {
+export async function updatePageData(name: string, newPage: Page): Promise> {
try {
- const page = pageData.page;
- const response = await put(`/api/pageeditor/${page}`, pageData);
- const json = (await response.json()) as PageText;
+ const response = await put(`/api/pageeditor/${name}`, newPage, {
+ "Content-Type": "application/json",
+ });
+ const json = (await response.json()) as Page;
return { success: true, data: json };
} catch (error) {
return handleAPIError(error);
}
}
-
-// type UpdateEventDetailsRequest = {
-// _id: string;
-// name: string;
-// description: string;
-// guidelines: string;
-// date: string;
-// location: string;
-// imageURI: string;
-// volunteers: string[];
-// };
-
-// export async function updateEventDetails(
-// eventDetails: UpdateEventDetailsRequest,
-// ): Promise> {
-// try {
-// const id = eventDetails._id;
-// const response = await put(`/api/eventDetails/${id}`, eventDetails, {
-// "Content-Type": "application/json",
-// });
-// const json = (await response.json()) as EventDetails;
-// return { success: true, data: json };
-// } catch (error) {
-// return handleAPIError(error);
-// }
-// }
From b0bc17eb1b689d3dd5c4a6cb42846489ad1bdf94 Mon Sep 17 00:00:00 2001
From: Jack Hansen <47556286+jackavh@users.noreply.github.com>
Date: Wed, 29 May 2024 14:50:35 -0700
Subject: [PATCH 05/14] set up components for page editor image upload
---
frontend/next.config.js | 3 +-
frontend/package-lock.json | 45 ++++--
frontend/package.json | 3 +-
frontend/src/app/admin/util/pageeditUtil.ts | 149 ++++++++++++++++++
frontend/src/components/admin/Toast.tsx | 53 +++++++
.../admin/pageeditor/CancelButton.module.css | 29 ++++
.../admin/pageeditor/CancelButton.tsx | 29 ++++
.../admin/pageeditor/Collapsible.module.css | 67 ++++++++
.../admin/pageeditor/Collapsible.tsx | 38 +++++
.../admin/pageeditor/CollapsibleFields.tsx | 49 ++++++
.../admin/pageeditor/PageProvider.tsx | 98 ++++++++++++
.../admin/pageeditor/SaveButton.module.css | 26 +++
.../admin/pageeditor/SaveButton.tsx | 30 ++++
.../defaultPages/aboutPageDefault.json | 87 ++++++++++
.../defaultPages/contactPageDefault.json | 41 +++++
.../defaultPages/eventsPageDefault.json | 35 ++++
.../defaultPages/homePageDefault.json | 57 +++++++
.../defaultPages/impactPageDefault.json | 35 ++++
.../defaultPages/involvedPageDefault.json | 35 ++++
.../defaultPages/missionPageDefault.json | 92 +++++++++++
.../defaultPages/newsletterPageDefault.json | 35 ++++
.../inputBoxes/GalleryBox.module.css | 10 ++
.../pageeditor/inputBoxes/GalleryBox.tsx | 124 +++++++++++++++
.../admin/pageeditor/inputBoxes/ImageBox.tsx | 15 ++
.../inputBoxes/TextFieldBox.module.css | 16 ++
.../pageeditor/inputBoxes/TextFieldBox.tsx | 43 +++++
.../admin/storage/DeleteModal.module.css | 15 ++
.../components/admin/storage/DeleteModal.tsx | 64 ++++++++
.../admin/storage/GalleryDropzone.tsx | 131 +++++++++++++++
.../admin/storage/ImageDropzone.tsx | 139 ++++++++++++++++
.../components/admin/storage/imageIcons.tsx | 60 +++++++
31 files changed, 1641 insertions(+), 12 deletions(-)
create mode 100644 frontend/src/app/admin/util/pageeditUtil.ts
create mode 100644 frontend/src/components/admin/Toast.tsx
create mode 100644 frontend/src/components/admin/pageeditor/CancelButton.module.css
create mode 100644 frontend/src/components/admin/pageeditor/CancelButton.tsx
create mode 100644 frontend/src/components/admin/pageeditor/Collapsible.module.css
create mode 100644 frontend/src/components/admin/pageeditor/Collapsible.tsx
create mode 100644 frontend/src/components/admin/pageeditor/CollapsibleFields.tsx
create mode 100644 frontend/src/components/admin/pageeditor/PageProvider.tsx
create mode 100644 frontend/src/components/admin/pageeditor/SaveButton.module.css
create mode 100644 frontend/src/components/admin/pageeditor/SaveButton.tsx
create mode 100644 frontend/src/components/admin/pageeditor/defaultPages/aboutPageDefault.json
create mode 100644 frontend/src/components/admin/pageeditor/defaultPages/contactPageDefault.json
create mode 100644 frontend/src/components/admin/pageeditor/defaultPages/eventsPageDefault.json
create mode 100644 frontend/src/components/admin/pageeditor/defaultPages/homePageDefault.json
create mode 100644 frontend/src/components/admin/pageeditor/defaultPages/impactPageDefault.json
create mode 100644 frontend/src/components/admin/pageeditor/defaultPages/involvedPageDefault.json
create mode 100644 frontend/src/components/admin/pageeditor/defaultPages/missionPageDefault.json
create mode 100644 frontend/src/components/admin/pageeditor/defaultPages/newsletterPageDefault.json
create mode 100644 frontend/src/components/admin/pageeditor/inputBoxes/GalleryBox.module.css
create mode 100644 frontend/src/components/admin/pageeditor/inputBoxes/GalleryBox.tsx
create mode 100644 frontend/src/components/admin/pageeditor/inputBoxes/ImageBox.tsx
create mode 100644 frontend/src/components/admin/pageeditor/inputBoxes/TextFieldBox.module.css
create mode 100644 frontend/src/components/admin/pageeditor/inputBoxes/TextFieldBox.tsx
create mode 100644 frontend/src/components/admin/storage/DeleteModal.module.css
create mode 100644 frontend/src/components/admin/storage/DeleteModal.tsx
create mode 100644 frontend/src/components/admin/storage/GalleryDropzone.tsx
create mode 100644 frontend/src/components/admin/storage/ImageDropzone.tsx
create mode 100644 frontend/src/components/admin/storage/imageIcons.tsx
diff --git a/frontend/next.config.js b/frontend/next.config.js
index b8f9012d..adddda7d 100644
--- a/frontend/next.config.js
+++ b/frontend/next.config.js
@@ -8,7 +8,8 @@ const nextConfig = {
"i.imgur.com",
"images.unsplash.com",
"plus.unsplash.com",
- "firebasestorage.googleapis.com"
+ "firebasestorage.googleapis.com",
+ "tse.ucsd.edu"
],
},
};
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index ee2028a0..5e844398 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -16,6 +16,7 @@
"@paypal/react-paypal-js": "^8.2.0",
"envalid": "^8.0.0",
"firebase": "^10.11.0",
+ "framer-motion": "^11.2.6",
"html2canvas": "^1.4.1",
"html2pdf.js": "^0.9.3",
"jspdf": "^2.5.1",
@@ -24,8 +25,8 @@
"nodemailer": "^6.9.11",
"react": "^18",
"react-dom": "^18",
- "react-firebase-hooks": "^5.1.1",
"react-dropzone": "^14.2.3",
+ "react-firebase-hooks": "^5.1.1",
"react-icons": "^5.2.1",
"react-material-symbols": "^4.3.1"
},
@@ -4100,6 +4101,30 @@
"url": "https://github.com/sponsors/rawify"
}
},
+ "node_modules/framer-motion": {
+ "version": "11.2.6",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.2.6.tgz",
+ "integrity": "sha512-XUrjjBt57e5YoHQtjwc3eNchFBuHvIgN/cS8SC4oIaAn2J/0+bLanUxXizidJKZVeHJam/JrmMnPRjYMglVn5g==",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -6089,15 +6114,6 @@
"react": "^18.2.0"
}
},
- "node_modules/react-firebase-hooks": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/react-firebase-hooks/-/react-firebase-hooks-5.1.1.tgz",
- "integrity": "sha512-y2UpWs82xs+39q5Rc/wq316ca52QsC0n8m801V+yM4IC4hbfOL4yQPVSh7w+ydstdvjN9F+lvs1WrO2VYxpmdA==",
- "peerDependencies": {
- "firebase": ">= 9.0.0",
- "react": ">= 16.8.0"
- }
- },
"node_modules/react-dropzone": {
"version": "14.2.3",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
@@ -6114,6 +6130,15 @@
"react": ">= 16.8 || 18.0.0"
}
},
+ "node_modules/react-firebase-hooks": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/react-firebase-hooks/-/react-firebase-hooks-5.1.1.tgz",
+ "integrity": "sha512-y2UpWs82xs+39q5Rc/wq316ca52QsC0n8m801V+yM4IC4hbfOL4yQPVSh7w+ydstdvjN9F+lvs1WrO2VYxpmdA==",
+ "peerDependencies": {
+ "firebase": ">= 9.0.0",
+ "react": ">= 16.8.0"
+ }
+ },
"node_modules/react-icons": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index e1db496c..bf56e525 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -22,6 +22,7 @@
"@paypal/react-paypal-js": "^8.2.0",
"envalid": "^8.0.0",
"firebase": "^10.11.0",
+ "framer-motion": "^11.2.6",
"html2canvas": "^1.4.1",
"html2pdf.js": "^0.9.3",
"jspdf": "^2.5.1",
@@ -30,8 +31,8 @@
"nodemailer": "^6.9.11",
"react": "^18",
"react-dom": "^18",
- "react-firebase-hooks": "^5.1.1",
"react-dropzone": "^14.2.3",
+ "react-firebase-hooks": "^5.1.1",
"react-icons": "^5.2.1",
"react-material-symbols": "^4.3.1"
},
diff --git a/frontend/src/app/admin/util/pageeditUtil.ts b/frontend/src/app/admin/util/pageeditUtil.ts
new file mode 100644
index 00000000..1a163360
--- /dev/null
+++ b/frontend/src/app/admin/util/pageeditUtil.ts
@@ -0,0 +1,149 @@
+import { deleteObject, getDownloadURL, getStorage, listAll, ref } from "firebase/storage";
+
+import { Field, GalleryData, ImageData, Page, TextData } from "../../../api/pageeditor";
+
+/**
+ * generateFieldMap
+ * Generates a mapping from field names to their index
+ * in the page.fields array. For example, if we wanted
+ * the field with the name "Image Gallery" :
+ *
+ * const imageGalleryField = page.fields[map.get("Image Gallery")]
+ *
+ * @param page
+ * @returns Map object
+ */
+export function generateFieldMap(page: Page) {
+ const map = new Map();
+ page.fields.forEach((field, index) => {
+ map.set(field.name, index);
+ });
+ return map;
+}
+
+/**
+ * generatePageMap
+ * Generates a mapping from field names to the data that they
+ * hold. For example, if `const map = generatePageMap(page)`,
+ * then `map.get("Body Text")` would return the associated text.
+ *
+ * @param page Page object to generate map for
+ * @returns Map object
+ */
+export function generatePageMap(page: Page) {
+ const map = new Map();
+
+ page.fields.forEach((f: Field) => {
+ const name = f.name;
+ if (f.type === "text") {
+ const text = (f.data as TextData).text;
+ map.set(name, text);
+ } else if (f.type === "image") {
+ const image = (f.data as ImageData).image;
+ map.set(name, image);
+ } else if (f.type === "gallery") {
+ const images = (f.data as GalleryData).images;
+ map.set(name, images);
+ }
+ });
+
+ return map;
+}
+
+/**
+ * sanitizeFilename
+ * Takes a string and parses it to be friendly
+ * as a filename for Firebase storage
+ * @param s
+ * @returns
+ */
+export function sanitizeFilename(s: string) {
+ const illegalRe = /[/?<>\\:*|"]/g; // illegal OS characters
+ const controlRe = /\p{Cc}/gu; // remove unicode control codes
+ const reservedRe = /^\.+$/; // reserved unix filenames
+ const windowsRe = /^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i; // reserved windows filenames
+ const trailDotRe = /[. ]+$/g; // remove trailing periods or spaces
+
+ const sanitized = s
+ .trim()
+ .replace(/\s/g, "_") // spaces -> _
+ .replace(illegalRe, "")
+ .replace(controlRe, "")
+ .replace(reservedRe, "")
+ .replace(windowsRe, "")
+ .replace(trailDotRe, "");
+
+ if (sanitized.length >= 256) return sanitized.substring(0, 255);
+ else return sanitized;
+}
+
+/**
+ * deleteFile
+ * Deletes a file in Firebase storage
+ * by its download url
+ * @param url
+ */
+export async function deleteFile(url: string) {
+ const storage = getStorage();
+ const imRef = ref(storage, url);
+
+ await deleteObject(imRef);
+}
+
+/**
+ * deleteFiles
+ * Deletes a string of files from Firebase storage
+ *
+ * @param urls
+ */
+export async function deleteFiles(urls: string[]) {
+ await Promise.all(urls.map((url) => deleteFile(url)));
+}
+
+/**
+ * createUniqueFilename
+ * Sanitizes fname string and checks against other
+ * files in the target folder for duplicate names.
+ *
+ * @param folder name of folder
+ * @param fname file name of uploaded file
+ * @returns unique and sanitized filename safe for upload
+ */
+export async function createUniqueFilename(folder: string, fname: string) {
+ const storage = getStorage();
+ const extRe = /(?=\.[^/.]+$)/g; // regex to split filename and extension
+ const [origName, ext] = sanitizeFilename(fname).split(extRe);
+ const listRef = ref(storage, folder);
+
+ let count = 0;
+ let fileName = origName;
+ await listAll(listRef)
+ .then((result) => {
+ while (result.items.some((itemRef) => itemRef.name === `${fileName}${ext}`)) {
+ fileName = `${origName}_${++count}`;
+ }
+ })
+ .catch(console.error);
+
+ return `${fileName}${ext}`;
+}
+
+/**
+ * listAllUrls
+ * List all image URLs in a given folder.
+ *
+ * @param folder folder name
+ * @returns string[] of urls
+ */
+export async function listAllUrls(folder: string) {
+ const storage = getStorage();
+ const listRef = ref(storage, folder);
+
+ const urls = await Promise.all(
+ (await listAll(listRef)).items.map((item) => {
+ return getDownloadURL(item);
+ }),
+ );
+
+ return urls;
+}
diff --git a/frontend/src/components/admin/Toast.tsx b/frontend/src/components/admin/Toast.tsx
new file mode 100644
index 00000000..d77fa1b5
--- /dev/null
+++ b/frontend/src/components/admin/Toast.tsx
@@ -0,0 +1,53 @@
+import CloseRoundedIcon from "@mui/icons-material/CloseRounded";
+import { Slide, SlideProps, Snackbar, createTheme } from "@mui/material";
+
+type ToastProps = {
+ message: string;
+ open: boolean;
+ handleClose: () => void;
+ Icon?: React.ComponentType;
+};
+
+const theme = createTheme({
+ transitions: {
+ easing: {
+ easeInOut: "cubic-bezier( 0.68, -0.55, 0.265, 1.55 )",
+ easeOut: "cubic-bezier(0.95, 0.05, 0.795, 0.035);",
+ },
+ },
+});
+
+function SlideTransition(props: SlideProps) {
+ return (
+
+ );
+}
+
+export default function Toast({ message, open, handleClose, Icon }: ToastProps) {
+ return (
+
+
+ {Icon &&
}
+
+ {message}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/admin/pageeditor/CancelButton.module.css b/frontend/src/components/admin/pageeditor/CancelButton.module.css
new file mode 100644
index 00000000..2be6da67
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/CancelButton.module.css
@@ -0,0 +1,29 @@
+.button {
+ width: max-content;
+ height: 48px;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ background: white;
+ border: 2px solid var(--color-primary-purple-hover);
+ border-radius: 4px;
+ color: var(--color-primary-purple-hover);
+}
+
+.button:hover {
+ background: white;
+}
+
+.buttonBody {
+ font: var(--font-body);
+ font-style: normal;
+ font-weight: 650;
+ font-size: 20px;
+ letter-spacing: 0.5px;
+ text-align: center;
+
+ padding: 12px 24px;
+ white-space: nowrap;
+ border: 2px var(--color-primary-purple-hover);
+}
diff --git a/frontend/src/components/admin/pageeditor/CancelButton.tsx b/frontend/src/components/admin/pageeditor/CancelButton.tsx
new file mode 100644
index 00000000..4e2c792f
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/CancelButton.tsx
@@ -0,0 +1,29 @@
+import { usePage } from "../../../components/admin/pageeditor/PageProvider";
+
+import styles from "./CancelButton.module.css";
+
+type ButtonProps = {
+ text: string;
+ onClick?: () => void;
+};
+
+const CancelButton = ({ text, onClick }: ButtonProps) => {
+ const page = usePage();
+ const color = page.isEdited ? "active" : "unactive";
+ return (
+
+
+ {text}
+
+
+ );
+};
+
+export default CancelButton;
diff --git a/frontend/src/components/admin/pageeditor/Collapsible.module.css b/frontend/src/components/admin/pageeditor/Collapsible.module.css
new file mode 100644
index 00000000..63862479
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/Collapsible.module.css
@@ -0,0 +1,67 @@
+@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Roboto+Slab:wght@100..900&display=swap");
+
+.collapsible {
+ background-color: #f3edf9;
+ cursor: pointer;
+
+ width: 100%;
+ height: 38px;
+ padding: 7px 12px;
+ border: none;
+ text-align: left;
+ outline: none;
+ font: var(--font-body-reg);
+
+ display: flex;
+ direction: row;
+ align-items: center;
+ gap: 8px; /* Between Arrow, Bar title */
+}
+
+.content {
+ font: var(--font-body);
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 20px;
+ padding: 16px 32px 0px;
+ background-color: #ffffff;
+ color: #484848;
+ display: flex;
+ flex-direction: column;
+ gap: 14px;
+}
+
+.title {
+ font: var(--font-body);
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 24px;
+}
+
+.subtitle {
+ font: var(--font-body);
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 24px;
+ margin-bottom: 2px;
+}
+
+.basicInput {
+ font-size: 14px;
+ width: 100%;
+ border: 1px solid #d8d8d8;
+ border-radius: 4px;
+ padding: 6px 12px;
+}
+
+.imageInput {
+ font-size: 14px;
+ width: auto;
+ height: auto;
+ border: 1px solid #d8d8d8;
+ border-radius: 4px;
+ padding: 6px 12px;
+}
diff --git a/frontend/src/components/admin/pageeditor/Collapsible.tsx b/frontend/src/components/admin/pageeditor/Collapsible.tsx
new file mode 100644
index 00000000..704d4297
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/Collapsible.tsx
@@ -0,0 +1,38 @@
+"use client";
+
+import Image from "next/image";
+import React, { ReactNode, useState } from "react";
+
+import styles from "./Collapsible.module.css";
+
+type CollapsibleProps = {
+ title: string;
+ children: ReactNode;
+};
+
+export const Collapsible = ({ title, children }: CollapsibleProps) => {
+ const [open, setOpen] = useState(true);
+
+ function toggleOpen() {
+ setOpen(!open);
+ }
+
+ return (
+
+
+ {open && (
+
+ )}
+ {!open && }
+ {title}
+
+ {open && children}
+
+ );
+};
diff --git a/frontend/src/components/admin/pageeditor/CollapsibleFields.tsx b/frontend/src/components/admin/pageeditor/CollapsibleFields.tsx
new file mode 100644
index 00000000..4ef10c51
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/CollapsibleFields.tsx
@@ -0,0 +1,49 @@
+import { generateFieldMap } from "../../../app/admin/util/pageeditUtil";
+
+import { Collapsible } from "./Collapsible";
+import { usePage } from "./PageProvider";
+import { GalleryBox } from "./inputBoxes/GalleryBox";
+import { ImageBox } from "./inputBoxes/ImageBox";
+import { TextFieldBox } from "./inputBoxes/TextFieldBox";
+
+type CollapsibleFieldsType = {
+ title: string;
+ fieldNames: string[];
+};
+
+export const CollapsibleFields = ({ title, fieldNames }: CollapsibleFieldsType) => {
+ const page = usePage();
+
+ // Convert a list of names to a list of Field objects with a hashmap
+ const fieldMap = generateFieldMap(page);
+ const fields = fieldNames.map((name) => {
+ const mappedIndex = fieldMap.get(name);
+ if (mappedIndex === undefined) {
+ throw new Error(`Error: ${name} is not a valid field on page ${page.name}`);
+ }
+
+ return page.fields[mappedIndex];
+ });
+
+ return (
+
+
+ {fields.map((field) => {
+ switch (field.type) {
+ case "text": {
+ return ;
+ }
+ case "image": {
+ return ;
+ }
+ case "gallery": {
+ return ;
+ }
+ default: {
+ throw new Error("Error: Unrecognized collapsible field");
+ }
+ }
+ })}
+
+ );
+};
diff --git a/frontend/src/components/admin/pageeditor/PageProvider.tsx b/frontend/src/components/admin/pageeditor/PageProvider.tsx
new file mode 100644
index 00000000..fcdde857
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/PageProvider.tsx
@@ -0,0 +1,98 @@
+import { Dispatch, ReactNode, createContext, useContext, useReducer } from "react";
+
+import { Field, Page } from "../../../api/pageeditor";
+
+// Action argument typing for the reducer function
+export type PageAction = {
+ type: string;
+ setIsEdited?: boolean;
+ page?: Page;
+ field?: Field;
+};
+
+// Placeholder page to initialize the context with
+const emptyPage: Page = {
+ name: "",
+ isEdited: false,
+ fields: [],
+};
+
+export const PageContext = createContext(emptyPage);
+export const PageDispatchContext = createContext>(() => {
+ console.log("Error: Page Dispatch is uninitialized.");
+});
+
+type PageProviderProps = {
+ initialPage: Page;
+ children: ReactNode;
+};
+
+function pageReducer(page: Page, action: PageAction): Page {
+ let newIsEdited;
+ // use setIsEdited if defined
+ if (action.setIsEdited !== undefined) {
+ newIsEdited = action.setIsEdited;
+ } else {
+ newIsEdited = page.isEdited;
+ }
+
+ if (action.field) {
+ // if field is defined, handle single field edit actions
+ const newField = action.field;
+ switch (action.type) {
+ case "edit_field": {
+ return {
+ ...page,
+ isEdited: newIsEdited,
+ fields: page.fields.map((f: Field) => (newField.name === f.name ? newField : f)),
+ };
+ }
+ default: {
+ throw new Error(`Error: ${action.type} is an unknown action type.`);
+ }
+ }
+ } else if (action.page) {
+ // if page is defined, handle full page edit actions
+ const newPage = action.page;
+ switch (action.type) {
+ case "edit_page": {
+ return {
+ ...page, // don't edit page.name
+ isEdited: newIsEdited,
+ fields: newPage.fields,
+ };
+ }
+ default: {
+ throw new Error(`Error: ${action.type} is an unknown action type.`);
+ }
+ }
+ } else {
+ // handle generic dispatches
+ switch (action.type) {
+ case "set_isEdited": {
+ return {
+ ...page,
+ isEdited: newIsEdited,
+ };
+ }
+ default: {
+ throw new Error(`Error: ${action.type} is an unknown action type.`);
+ }
+ }
+ }
+}
+
+// Context provider for page editors
+export function PageProvider({ initialPage, children }: PageProviderProps) {
+ const [page, dispatch] = useReducer(pageReducer, initialPage);
+
+ return (
+
+ {children}
+
+ );
+}
+
+// Hooks for Page context and Reducer Dispatch context
+export const usePage = () => useContext(PageContext);
+export const usePageDispatch = () => useContext(PageDispatchContext);
diff --git a/frontend/src/components/admin/pageeditor/SaveButton.module.css b/frontend/src/components/admin/pageeditor/SaveButton.module.css
new file mode 100644
index 00000000..1ae8a607
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/SaveButton.module.css
@@ -0,0 +1,26 @@
+.button {
+ width: max-content;
+ height: 48px;
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ align-items: center;
+ background: var(--color-primary-purple);
+ border-radius: 4px;
+}
+
+.button:hover {
+ background: var(--color-primary-purple-hover);
+}
+
+.buttonBody {
+ font: var(--font-body);
+ font-style: normal;
+ font-weight: 650;
+ font-size: 20px;
+ letter-spacing: 0.5px;
+ text-align: center;
+ color: white;
+ padding: 12px 24px;
+ white-space: nowrap;
+}
diff --git a/frontend/src/components/admin/pageeditor/SaveButton.tsx b/frontend/src/components/admin/pageeditor/SaveButton.tsx
new file mode 100644
index 00000000..885e0402
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/SaveButton.tsx
@@ -0,0 +1,30 @@
+import { usePage } from "../../../components/admin/pageeditor/PageProvider";
+
+import styles from "./SaveButton.module.css";
+
+type ButtonProps = {
+ text: string;
+ onClick?: () => void;
+};
+
+const SaveButton = ({ text, onClick }: ButtonProps) => {
+ const page = usePage();
+ const color = page.isEdited ? "active" : "unactive";
+ return (
+
+
+ {text}
+
+
+ );
+};
+
+export default SaveButton;
diff --git a/frontend/src/components/admin/pageeditor/defaultPages/aboutPageDefault.json b/frontend/src/components/admin/pageeditor/defaultPages/aboutPageDefault.json
new file mode 100644
index 00000000..f07876a6
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/defaultPages/aboutPageDefault.json
@@ -0,0 +1,87 @@
+{
+ "name": "about",
+ "isEdited": false,
+ "fields": [
+ {
+ "name": "Subtitle",
+ "type": "text",
+ "data": {
+ "text": "4FLOT is committed in preventing and ending homelessness, hunger and disparity in underprivileged communities."
+ }
+ },
+ {
+ "name": "Header Image Carousel",
+ "type": "gallery",
+ "data": {
+ "images": [],
+ "maxImages": 4
+ }
+ },
+ {
+ "name": "Mission Section Title",
+ "type": "text",
+ "data": {
+ "text": "Why We Do It"
+ }
+ },
+ {
+ "name": "Mission Body Text",
+ "type": "text",
+ "data": {
+ "text": "Leading the way for generations to come! Together we can .... make a difference by paying it forward with Love, Compassion, and Community Outreach for all humanity."
+ }
+ },
+ {
+ "name": "Mission Section Image",
+ "type": "image",
+ "data": {
+ "image": "",
+ "hasImage": false
+ }
+ },
+ {
+ "name": "Team Section Title",
+ "type": "text",
+ "data": {
+ "text": "Meet our Team"
+ }
+ },
+ {
+ "name": "Team Body Text",
+ "type": "text",
+ "data": {
+ "text": "Lorem ipsum dolor sit amet consectetur. Et vestibulum enim nunc ultrices. Donec blandit sollicitudin vitae integer mauris sed. Mattis duis id viverra suscipit morbi."
+ }
+ },
+ {
+ "name": "Team Section Image",
+ "type": "image",
+ "data": {
+ "image": "",
+ "hasImage": false
+ }
+ },
+ {
+ "name": "Contact Section Title",
+ "type": "text",
+ "data": {
+ "text": "Stay Connected"
+ }
+ },
+ {
+ "name": "Contact Body Text",
+ "type": "text",
+ "data": {
+ "text": "Lorem ipsum dolor sit amet consectetur. Et vestibulum enim nunc ultrices. Donec blandit sollicitudin vitae integer mauris sed. Mattis duis id viverra suscipit morbi."
+ }
+ },
+ {
+ "name": "Contact Section Image",
+ "type": "image",
+ "data": {
+ "image": "",
+ "hasImage": false
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frontend/src/components/admin/pageeditor/defaultPages/contactPageDefault.json b/frontend/src/components/admin/pageeditor/defaultPages/contactPageDefault.json
new file mode 100644
index 00000000..de820a2c
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/defaultPages/contactPageDefault.json
@@ -0,0 +1,41 @@
+{
+ "name": "contact",
+ "isEdited": false,
+ "fields": [
+ {
+ "name": "Section Title",
+ "type": "text",
+ "data": {
+ "text": "There are many ways to join us and support our mission. Contact us to find out more about volunteering opportunities, fundraising, and more !"
+ }
+ },
+ {
+ "name": "Body Text",
+ "type": "text",
+ "data": {
+ "text": "There are many ways to join us and support our mission. Contact us to find out more about volunteering opportunities, fundraising, and more !"
+ }
+ },
+ {
+ "name": "Phone Number",
+ "type": "text",
+ "data": {
+ "text": "909-757-1313"
+ }
+ },
+ {
+ "name": "Locations",
+ "type": "text",
+ "data": {
+ "text": "San Bernadino County\nRiverside County\nLos Angeles County"
+ }
+ },
+ {
+ "name": "Email",
+ "type": "text",
+ "data": {
+ "text": "admin@4flot.com"
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frontend/src/components/admin/pageeditor/defaultPages/eventsPageDefault.json b/frontend/src/components/admin/pageeditor/defaultPages/eventsPageDefault.json
new file mode 100644
index 00000000..931bfa40
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/defaultPages/eventsPageDefault.json
@@ -0,0 +1,35 @@
+{
+ "name": "events",
+ "isEdited": false,
+ "fields": [
+ {
+ "name": "Subtitle",
+ "type": "text",
+ "data": {
+ "text": "Lorem ipsum dolor sit amet consectetur. Et vestibulum enim nunc ultrices. Donec blandit sollicitudin vitae integer mauris sed. Mattis duis id viverra suscipit morbi."
+ }
+ },
+ {
+ "name": "Header Image Carousel",
+ "type": "gallery",
+ "data": {
+ "images": [],
+ "maxImages": 4
+ }
+ },
+ {
+ "name": "Section Title",
+ "type": "text",
+ "data": {
+ "text": "Volunteer With Us"
+ }
+ },
+ {
+ "name": "Section Subtitle",
+ "type": "text",
+ "data": {
+ "text": "Lorem ipsum dolor sit amet consectetur. Et vestibulum enim nunc ultrices. Donec blandit sollicitudin vitae integer mauris sed. Mattis duis id viverra suscipit morbi."
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frontend/src/components/admin/pageeditor/defaultPages/homePageDefault.json b/frontend/src/components/admin/pageeditor/defaultPages/homePageDefault.json
new file mode 100644
index 00000000..df9726b8
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/defaultPages/homePageDefault.json
@@ -0,0 +1,57 @@
+ {
+ "name": "home",
+ "isEdited": false,
+ "fields": [
+ {
+ "name": "Subtitle",
+ "type": "text",
+ "data": {
+ "text": "4FLOT is committed in preventing and ending homelessness, hunger and disparity in underprivileged communities."
+ }
+ },
+ {
+ "name": "Header Image Carousel",
+ "type": "gallery",
+ "data": {
+ "images": [],
+ "maxImages": 4
+ }
+ },
+ {
+ "name": "Events Section Title",
+ "type": "text",
+ "data": {
+ "text": "Get Involved at our Upcoming Events"
+ }
+ },
+ {
+ "name": "Events Body Text",
+ "type": "text",
+ "data": {
+ "text": "Lorem ipsum dolor sit amet consectetur. Et vestibulum enim nunc ultrices. Donec blandit sollicitudin vitae integer mauris sed. Mattis duis id viverra suscipit morbi."
+ }
+ },
+ {
+ "name": "Sponsors Section Title",
+ "type": "text",
+ "data": {
+ "text": "Our Community Sponsors"
+ }
+ },
+ {
+ "name": "Sponsors Body Text",
+ "type": "text",
+ "data": {
+ "text": "Lorem ipsum dolor sit amet consectetur. Et vestibulum enim nunc ultrices. Donec blandit sollicitudin vitae integer mauris sed. Mattis duis id viverra suscipit morbi."
+ }
+ },
+ {
+ "name": "Sponsor Image Gallery",
+ "type": "gallery",
+ "data": {
+ "images": [],
+ "maxImages": -1
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frontend/src/components/admin/pageeditor/defaultPages/impactPageDefault.json b/frontend/src/components/admin/pageeditor/defaultPages/impactPageDefault.json
new file mode 100644
index 00000000..bfbc0b2a
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/defaultPages/impactPageDefault.json
@@ -0,0 +1,35 @@
+{
+ "name": "impact",
+ "isEdited": false,
+ "fields": [
+ {
+ "name": "Subtitle",
+ "type": "text",
+ "data": {
+ "text": "4FLOT is committed in preventing and ending homelessness, hunger and disparity in underprivileged communities. "
+ }
+ },
+ {
+ "name": "Header Image Carousel",
+ "type": "gallery",
+ "data": {
+ "images": [],
+ "maxImages": 4
+ }
+ },
+ {
+ "name": "Testimonials Subtitle",
+ "type": "text",
+ "data": {
+ "text": "Lorem ipsum dolor sit amet consectetur. Et vestibulum enim nunc ultrices. Donec blandit sollicitudin vitae integer mauris sed. Mattis duis id viverra suscipit morbi."
+ }
+ },
+ {
+ "name": "Newsletter Subtitle",
+ "type": "text",
+ "data": {
+ "text": "Lorem ipsum dolor sit amet consectetur. Et vestibulum enim nunc ultrices. Donec blandit sollicitudin vitae integer mauris sed. Mattis duis id viverra suscipit morbi."
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frontend/src/components/admin/pageeditor/defaultPages/involvedPageDefault.json b/frontend/src/components/admin/pageeditor/defaultPages/involvedPageDefault.json
new file mode 100644
index 00000000..602f84c0
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/defaultPages/involvedPageDefault.json
@@ -0,0 +1,35 @@
+{
+ "name": "involved",
+ "isEdited": false,
+ "fields": [
+ {
+ "name": "Subtitle",
+ "type": "text",
+ "data": {
+ "text": "4FLOT is committed in preventing and ending homelessness, hunger and disparity in underprivileged communities."
+ }
+ },
+ {
+ "name": "Header Image Carousel",
+ "type": "gallery",
+ "data": {
+ "images": [],
+ "maxImages": 4
+ }
+ },
+ {
+ "name": "Events Subtitle",
+ "type": "text",
+ "data": {
+ "text": "Lorem ipsum dolor sit amet consectetur. Et vestibulum enim nunc ultrices. Donec blandit sollicitudin vitae integer mauris sed. Mattis duis id viverra suscipit morbi."
+ }
+ },
+ {
+ "name": "Donations Subtitle",
+ "type": "text",
+ "data": {
+ "text": "Your support and contributions will enable us to meet our goals and improve conditions. Your generous donation will fund our mission."
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frontend/src/components/admin/pageeditor/defaultPages/missionPageDefault.json b/frontend/src/components/admin/pageeditor/defaultPages/missionPageDefault.json
new file mode 100644
index 00000000..c0fc6d97
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/defaultPages/missionPageDefault.json
@@ -0,0 +1,92 @@
+{
+ "name": "mission",
+ "isEdited": false,
+ "fields": [
+ {
+ "name": "Subtitle",
+ "type": "text",
+ "data": {
+ "text": "Leading the way for generations to come! Together we can make a difference by paying it forward with Service, Compassion, and Community for all humanity."
+ }
+ },
+ {
+ "name": "Values Section Title",
+ "type": "text",
+ "data": {
+ "text": "We pay it forward with..."
+ }
+ },
+ {
+ "name": "Value #1",
+ "type": "text",
+ "data": {
+ "text": "Service"
+ }
+ },
+ {
+ "name": "Value #1 Description",
+ "type": "text",
+ "data": {
+ "text": "Lorem ipsum dolor sit amet consectetur. Et vestibulum enim nunc ultrices. Donec blandit sollicitudin vitae integer mauris sed. Mattis duis id viverra suscipit morbi.."
+ }
+ },
+ {
+ "name": "Value #2",
+ "type": "text",
+ "data": {
+ "text": "Compassion"
+ }
+ },
+ {
+ "name": "Value #2 Description",
+ "type": "text",
+ "data": {
+ "text": "Lorem ipsum dolor sit amet consectetur. Et vestibulum enim nunc ultrices. Donec blandit sollicitudin vitae integer mauris sed. Mattis duis id viverra suscipit morbi.."
+ }
+ },
+ {
+ "name": "Value #3",
+ "type": "text",
+ "data": {
+ "text": "Community"
+ }
+ },
+ {
+ "name": "Value #3 Description",
+ "type": "text",
+ "data": {
+ "text": "Lorem ipsum dolor sit amet consectetur. Et vestibulum enim nunc ultrices. Donec blandit sollicitudin vitae integer mauris sed. Mattis duis id viverra suscipit morbi.."
+ }
+ },
+ {
+ "name": "Story Section Title",
+ "type": "text",
+ "data": {
+ "text": "Here's Our Story"
+ }
+ },
+ {
+ "name": "Body Text",
+ "type": "text",
+ "data": {
+ "text": "At one point or another, each of the founding members have gone through personal struggles, some have experienced homelessness, hunger, medical illnesses and others juggled single parenting, while furthering their education, and so on. However, the common denominator was that each of us needed Help. So now we are \"The Helpers\" 4 Future Leaders of Tomorrow -because the people we help are our future"
+ }
+ },
+ {
+ "name": "Image Gallery",
+ "type": "gallery",
+ "data": {
+ "images": [],
+ "maxImages": 3
+ }
+ },
+ {
+ "name": "Header Image Carousel",
+ "type": "gallery",
+ "data": {
+ "images": [],
+ "maxImages": 4
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frontend/src/components/admin/pageeditor/defaultPages/newsletterPageDefault.json b/frontend/src/components/admin/pageeditor/defaultPages/newsletterPageDefault.json
new file mode 100644
index 00000000..f22a613d
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/defaultPages/newsletterPageDefault.json
@@ -0,0 +1,35 @@
+{
+ "name": "newsletter",
+ "isEdited": false,
+ "fields": [
+ {
+ "name": "Subtitle",
+ "type": "text",
+ "data": {
+ "text": "Lorem ipsum dolor sit amet consectetur. Et vestibulum enim nunc ultrices. Donec blandit sollicitudin vitae integer mauris sed. Mattis duis id viverra suscipit morbi."
+ }
+ },
+ {
+ "name": "Header Image Carousel",
+ "type": "gallery",
+ "data": {
+ "images": [],
+ "maxImages": 4
+ }
+ },
+ {
+ "name": "Section Title",
+ "type": "text",
+ "data": {
+ "text": "Quarterly Updates"
+ }
+ },
+ {
+ "name": "Section Subtitle",
+ "type": "text",
+ "data": {
+ "text": "Description of general newsletter content, what to expect in the newsletters, etc."
+ }
+ }
+ ]
+}
\ No newline at end of file
diff --git a/frontend/src/components/admin/pageeditor/inputBoxes/GalleryBox.module.css b/frontend/src/components/admin/pageeditor/inputBoxes/GalleryBox.module.css
new file mode 100644
index 00000000..e9fb301c
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/inputBoxes/GalleryBox.module.css
@@ -0,0 +1,10 @@
+@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Roboto+Slab:wght@100..900&display=swap");
+
+.subtitle {
+ font: var(--font-body);
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 24px;
+ margin-bottom: 2px;
+}
diff --git a/frontend/src/components/admin/pageeditor/inputBoxes/GalleryBox.tsx b/frontend/src/components/admin/pageeditor/inputBoxes/GalleryBox.tsx
new file mode 100644
index 00000000..9be40d8b
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/inputBoxes/GalleryBox.tsx
@@ -0,0 +1,124 @@
+import { Reorder } from "framer-motion";
+import Image from "next/image";
+import { HiMiniMinusCircle } from "react-icons/hi2";
+
+import { Field, GalleryData, updatePageData } from "../../../../api/pageeditor";
+import { deleteFile } from "../../../../app/admin/util/pageeditUtil";
+import DeleteModal from "../../storage/DeleteModal";
+import GalleryDropzone from "../../storage/GalleryDropzone";
+import { usePage, usePageDispatch } from "../PageProvider";
+
+import styles from "./GalleryBox.module.css";
+
+type GalleryBoxProps = { field: Field };
+
+type GalleryImageProps = {
+ imageUrl: string;
+ handleDelete: () => void;
+};
+
+const GalleryImage = ({ imageUrl, handleDelete }: GalleryImageProps) => {
+ return (
+ <>
+
+
+
+
+
+
+ >
+ );
+};
+
+export const GalleryBox = ({ field }: GalleryBoxProps) => {
+ const page = usePage();
+ const dispatch = usePageDispatch();
+ const data = field.data as GalleryData;
+ const imageCount = data.images.length;
+ const hasMax = imageCount > 0;
+ const maxImages = data.maxImages;
+
+ function handleUpdateOrder(newOrder: string[]) {
+ dispatch({
+ type: "edit_field",
+ setIsEdited: true,
+ field: {
+ ...field,
+ data: {
+ ...data,
+ images: newOrder,
+ },
+ },
+ });
+ }
+
+ function handleDelete(imageUrl: string) {
+ // create shape of the edited field
+ const newField = {
+ ...field,
+ data: {
+ ...data, // remove the idx with imageUrl
+ images: data.images.filter((url) => url !== imageUrl),
+ },
+ };
+ // remove image from local state
+ dispatch({
+ type: "edit_field",
+ setIsEdited: false,
+ field: newField,
+ });
+ // remove image from mongodb
+ updatePageData(page.name, {
+ ...page,
+ isEdited: false,
+ fields: page.fields.map((f: Field) => (newField.name === f.name ? newField : f)),
+ })
+ .then(() => {
+ deleteFile(imageUrl).catch(console.error);
+ })
+ .catch(console.error);
+ }
+
+ return (
+
+
{field.name}
+
+
+ {data.images.map((imageUrl) => (
+
+
+ {
+ handleDelete(imageUrl);
+ }}
+ />
+
+
+ ))}
+
+
+
+
+
+ {hasMax && (
+
{`${imageCount}/${maxImages} Images`}
+ )}
+
+ );
+};
diff --git a/frontend/src/components/admin/pageeditor/inputBoxes/ImageBox.tsx b/frontend/src/components/admin/pageeditor/inputBoxes/ImageBox.tsx
new file mode 100644
index 00000000..e3087690
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/inputBoxes/ImageBox.tsx
@@ -0,0 +1,15 @@
+import { Field } from "../../../../api/pageeditor";
+import ImageDropzone from "../../storage/ImageDropzone";
+
+type ImageBoxProps = {
+ field: Field;
+};
+
+export const ImageBox = ({ field }: ImageBoxProps) => {
+ return (
+
+ );
+};
diff --git a/frontend/src/components/admin/pageeditor/inputBoxes/TextFieldBox.module.css b/frontend/src/components/admin/pageeditor/inputBoxes/TextFieldBox.module.css
new file mode 100644
index 00000000..85789c38
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/inputBoxes/TextFieldBox.module.css
@@ -0,0 +1,16 @@
+.subtitle {
+ font: var(--font-body);
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 24px;
+ margin-bottom: 2px;
+}
+
+.basicInput {
+ font-size: 14px;
+ width: 100%;
+ border: 1px solid #d8d8d8;
+ border-radius: 4px;
+ padding: 6px 12px;
+}
diff --git a/frontend/src/components/admin/pageeditor/inputBoxes/TextFieldBox.tsx b/frontend/src/components/admin/pageeditor/inputBoxes/TextFieldBox.tsx
new file mode 100644
index 00000000..66d11943
--- /dev/null
+++ b/frontend/src/components/admin/pageeditor/inputBoxes/TextFieldBox.tsx
@@ -0,0 +1,43 @@
+import { ChangeEvent } from "react";
+
+import { TextareaAutosize } from "@mui/material";
+
+import { Field, TextData } from "../../../../api/pageeditor";
+import { usePageDispatch } from "../PageProvider";
+
+import styles from "./TextFieldBox.module.css";
+
+type TextFieldProps = {
+ field: Field;
+};
+
+export const TextFieldBox = ({ field }: TextFieldProps) => {
+ const dispatch = usePageDispatch();
+
+ function handleFieldChange(e: ChangeEvent) {
+ // Auto increase height when typing
+ e.target.style.height = "auto";
+ e.target.style.height = 2 + e.target.scrollHeight + "px";
+
+ // dispatch a change to a text field
+ dispatch({
+ type: "edit_field",
+ setIsEdited: true,
+ field: {
+ ...field,
+ data: {
+ text: e.target.value,
+ },
+ },
+ });
+ }
+
+ const text = (field.data as TextData).text;
+
+ return (
+
+ );
+};
diff --git a/frontend/src/components/admin/storage/DeleteModal.module.css b/frontend/src/components/admin/storage/DeleteModal.module.css
new file mode 100644
index 00000000..37d65d98
--- /dev/null
+++ b/frontend/src/components/admin/storage/DeleteModal.module.css
@@ -0,0 +1,15 @@
+@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Roboto+Slab:wght@100..900&display=swap");
+
+.title {
+ font: var(--font-small-subtitle);
+ font-size: 1.45rem;
+ font-style: normal;
+ font-weight: 700;
+ line-height: 2.25rem;
+ letter-spacing: 0.03rem;
+ color: #000000;
+}
+
+.subtitle {
+ font: var(--font-body-reg);
+}
diff --git a/frontend/src/components/admin/storage/DeleteModal.tsx b/frontend/src/components/admin/storage/DeleteModal.tsx
new file mode 100644
index 00000000..e5fad622
--- /dev/null
+++ b/frontend/src/components/admin/storage/DeleteModal.tsx
@@ -0,0 +1,64 @@
+import CloseRoundedIcon from "@mui/icons-material/CloseRounded";
+import { Modal } from "@mui/material";
+import { ReactNode, useState } from "react";
+
+import styles from "./DeleteModal.module.css";
+
+type DeleteProps = {
+ handleDelete: () => void;
+ disabled: boolean;
+ children: ReactNode;
+};
+export default function DeleteModal({ handleDelete, disabled, children }: DeleteProps) {
+ const [open, setOpen] = useState(false);
+
+ const handleClick = () => {
+ setOpen(!open);
+ };
+
+ const handleClose = () => {
+ setOpen(false);
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
Are you sure you want to delete this photo?
+
This action is permanent and cannot be undone.
+
+
+
+ No, Cancel
+
+ {
+ handleClose();
+ handleDelete();
+ }}
+ className="m-0 w-[140px] h-10 rounded text-white bg-[#694C97] px-3 py-2 font-bold shadow-sm hover:bg-purple-900 hover:font-extrabold"
+ >
+ Delete Photo
+
+
+
+
+
+
+
+ {children}
+
+
+ );
+}
diff --git a/frontend/src/components/admin/storage/GalleryDropzone.tsx b/frontend/src/components/admin/storage/GalleryDropzone.tsx
new file mode 100644
index 00000000..1e520911
--- /dev/null
+++ b/frontend/src/components/admin/storage/GalleryDropzone.tsx
@@ -0,0 +1,131 @@
+"use client";
+
+import { getDownloadURL, getStorage, ref } from "firebase/storage";
+import React from "react";
+import Dropzone from "react-dropzone";
+import { useUploadFile } from "react-firebase-hooks/storage";
+
+import { Field, GalleryData } from "../../../api/pageeditor";
+import { createUniqueFilename } from "../../../app/admin/util/pageeditUtil";
+import { usePage, usePageDispatch } from "../pageeditor/PageProvider";
+
+import { UploadIcon } from "./imageIcons";
+
+type GalleryDropProps = {
+ field: Field;
+};
+
+type IconTextProps = {
+ capped: boolean;
+ uploading: boolean;
+ disabled: boolean;
+};
+
+function IconAndText({ capped, uploading, disabled }: IconTextProps) {
+ const color = disabled ? "#D2D2D2" : "#0370BB";
+ let message;
+ if (capped) {
+ message = "Maximum Reached";
+ } else if (uploading) {
+ message = "Uploading...";
+ } else {
+ message = "Add a Photo";
+ }
+
+ return (
+
+ );
+}
+
+export default function GalleryDropzone({ field }: GalleryDropProps) {
+ const storage = getStorage();
+ const [uploadFile, uploading, snapshot, error] = useUploadFile();
+ const accept = {
+ "image/*": [".jpeg", ".jpg", ".png"],
+ };
+
+ const page = usePage();
+ const dispatch = usePageDispatch();
+ const folder = page.name.toLowerCase().replace(/\s/g, "");
+ const data = field.data as GalleryData;
+
+ const hasMax = data.maxImages > 0;
+ const capped = hasMax && data.images.length >= data.maxImages;
+ const disabled = capped || uploading;
+
+ function handleAddImages(urls: string[]) {
+ dispatch({
+ type: "edit_field",
+ setIsEdited: true,
+ field: {
+ ...field,
+ data: {
+ ...field.data,
+ images: [...data.images, ...urls],
+ },
+ },
+ });
+ }
+
+ async function upload(file: File) {
+ // Upload file at reference in Firebase storage then get its URL
+ const fname = await createUniqueFilename(folder, file.name);
+ const storageRef = ref(storage, `${folder}/${fname}`);
+ await uploadFile(storageRef, file);
+ const url = await getDownloadURL(storageRef);
+
+ if (error) console.log(error);
+ if (snapshot) console.log(snapshot);
+
+ return url;
+ }
+
+ async function uploadFiles(files: File[]) {
+ const urls = await Promise.all(
+ files.map((file) => {
+ return upload(file);
+ }),
+ );
+
+ return urls;
+ }
+
+ function onDrop(files: File[]) {
+ uploadFiles(files)
+ .then((urls) => {
+ if (urls) {
+ handleAddImages(urls);
+ }
+ })
+ .catch(console.error);
+ }
+
+ return (
+
+
{
+ onDrop(files);
+ }}
+ disabled={disabled}
+ >
+ {({ getRootProps, getInputProps }) => (
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/frontend/src/components/admin/storage/ImageDropzone.tsx b/frontend/src/components/admin/storage/ImageDropzone.tsx
new file mode 100644
index 00000000..facf48b8
--- /dev/null
+++ b/frontend/src/components/admin/storage/ImageDropzone.tsx
@@ -0,0 +1,139 @@
+import { getDownloadURL, getStorage, ref } from "firebase/storage";
+import Dropzone from "react-dropzone";
+import { useUploadFile } from "react-firebase-hooks/storage";
+
+import { Field, ImageData, updatePageData } from "../../../api/pageeditor";
+import { createUniqueFilename, deleteFile } from "../../../app/admin/util/pageeditUtil";
+import { usePage, usePageDispatch } from "../pageeditor/PageProvider";
+
+import DeleteModal from "./DeleteModal";
+import { DeleteIcon, PhotoIcon, UploadIcon } from "./imageIcons";
+
+type ImageDropProps = {
+ field: Field;
+};
+
+export default function ImageDropzone({ field }: ImageDropProps) {
+ const storage = getStorage();
+ const [uploadFile, uploading, snapshot, error] = useUploadFile();
+ const accept = {
+ "image/*": [".jpeg", ".jpg", ".png"],
+ };
+
+ const page = usePage();
+ const dispatch = usePageDispatch();
+ const folder = page.name.toLowerCase().replace(/\s/g, "");
+ const data = field.data as ImageData;
+ const hasImage = data.hasImage;
+
+ function handleSetImage(url: string) {
+ if (!hasImage) {
+ dispatch({
+ type: "edit_field",
+ setIsEdited: true,
+ field: {
+ ...field,
+ data: {
+ ...field.data,
+ image: url,
+ hasImage: true,
+ },
+ },
+ });
+ }
+ }
+
+ // TODO: figure out if edited should be true or false after delete and add
+ function handleUnsetImage() {
+ // create shape of edited field
+ const newField = {
+ ...field,
+ data: {
+ ...data,
+ image: "",
+ hasImage: false,
+ },
+ };
+ // remove image string from local state
+ dispatch({
+ type: "edit_field",
+ setIsEdited: false,
+ field: newField,
+ });
+ // remove image string from mongodb
+ updatePageData(page.name, {
+ ...page,
+ isEdited: false,
+ fields: page.fields.map((f: Field) => (newField.name === f.name ? newField : f)),
+ })
+ .then(() => {
+ deleteFile(data.image).catch(console.error);
+ })
+ .catch(console.error);
+ }
+
+ async function upload(file: File) {
+ // Upload file at reference in Firebase storage then get its URL
+ const fname = await createUniqueFilename(folder, file.name);
+ const storageRef = ref(storage, `${folder}/${fname}`);
+ await uploadFile(storageRef, file);
+ const url = await getDownloadURL(storageRef);
+
+ if (error) console.log(error);
+ if (snapshot) console.log(snapshot);
+
+ return url;
+ }
+
+ function onDrop(acceptedFiles: File[]) {
+ const image = acceptedFiles[0];
+ upload(image)
+ .then((url) => {
+ handleSetImage(url);
+ })
+ .catch(console.error);
+ }
+
+ if (hasImage) {
+ return (
+
+
+
+ {ref(storage, data.image).name}
+
+
+
+
+
+ );
+ } else {
+ return (
+
+
{
+ onDrop(acceptedFiles);
+ }}
+ disabled={hasImage || uploading}
+ >
+ {({ getRootProps, getInputProps }) => (
+
+
+
+
+
+ {uploading ? (
+
Uploading...
+ ) : (
+ "Add a Photo"
+ )}
+
+
+
+
+ )}
+
+
+ );
+ }
+}
diff --git a/frontend/src/components/admin/storage/imageIcons.tsx b/frontend/src/components/admin/storage/imageIcons.tsx
new file mode 100644
index 00000000..ba99bb7c
--- /dev/null
+++ b/frontend/src/components/admin/storage/imageIcons.tsx
@@ -0,0 +1,60 @@
+import { SVGProps } from "react";
+
+type IconProps = {
+ color?: string;
+ props?: SVGProps;
+ width?: number;
+ height?: number;
+};
+
+export const DeleteIcon = ({ color, props, width, height }: IconProps) => (
+
+
+
+);
+
+export const UploadIcon = ({ color, props, width, height }: IconProps) => (
+
+
+
+
+);
+
+export const PhotoIcon = ({ color, props, width, height }: IconProps) => (
+
+
+
+);
From 0863ded5e2ab8ebf71f4e9eb03019ff70e76e5e9 Mon Sep 17 00:00:00 2001
From: Jack Hansen <47556286+jackavh@users.noreply.github.com>
Date: Wed, 29 May 2024 14:51:28 -0700
Subject: [PATCH 06/14] change web app pages to use images from firebase
---
frontend/src/app/(web app)/about/page.tsx | 80 ++++------
frontend/src/app/(web app)/contact/page.tsx | 44 ++++--
frontend/src/app/(web app)/impact/page.tsx | 58 +++-----
frontend/src/app/(web app)/involved/page.tsx | 56 +++----
.../src/app/(web app)/mission/page.module.css | 3 +
frontend/src/app/(web app)/mission/page.tsx | 137 +++++++-----------
.../src/app/(web app)/newsletter/page.tsx | 71 ++++-----
frontend/src/app/(web app)/page.tsx | 104 ++++++-------
.../app/(web app)/upcoming-events/page.tsx | 56 +++----
9 files changed, 259 insertions(+), 350 deletions(-)
diff --git a/frontend/src/app/(web app)/about/page.tsx b/frontend/src/app/(web app)/about/page.tsx
index 82995cf0..73e142e7 100644
--- a/frontend/src/app/(web app)/about/page.tsx
+++ b/frontend/src/app/(web app)/about/page.tsx
@@ -1,94 +1,72 @@
"use client";
+
import React, { useEffect, useState } from "react";
-import { getPageText } from "../../../api/pageeditor";
+import { getPageData } from "../../../api/pageeditor";
+import AboutCard from "../../../components/AboutCard";
+import BackgroundHeader from "../../../components/BackgroundHeader";
+import LoadingSpinner from "../../../components/admin/LoadingSpinner";
+import { generatePageMap } from "../../admin/util/pageeditUtil";
import styles from "./page.module.css";
-import { BackgroundImage, BackgroundImagePages, getBackgroundImages } from "@/api/images";
-import AboutCard from "@/components/AboutCard";
-import BackgroundHeader from "@/components/BackgroundHeader";
-
-export default function Impact() {
- const [images, setImages] = useState([]);
- const [phSubtitle, setPhSubtitle] = useState("");
- const [s1Subtitle, setS1Subtitle] = useState("");
- const [s1Text, setS1Text] = useState("");
- const [s2Subtitle, setS2Subtitle] = useState("");
- const [s2Text, setS2Text] = useState("");
- const [s3Subtitle, setS3Subtitle] = useState("");
- const [s3Text, setS3Text] = useState("");
-
- useEffect(() => {
- getBackgroundImages(BackgroundImagePages.TEAM)
- .then((result) => {
- if (result.success) {
- setImages(result.data);
- }
- })
- .catch((error) => {
- alert(error);
- });
- }, []);
+export default function AboutPage() {
+ const [pageMap, setPageMap] = useState>();
+ const [loading, setLoading] = useState(false);
- let pageText;
useEffect(() => {
- getPageText("About Us")
+ setLoading(true);
+ getPageData("about")
.then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Subtitle(pageText.pageSections[1].sectionTitle ?? "");
- setS1Text(pageText.pageSections[1].sectionSubtitle ?? "");
- setS2Subtitle(pageText.pageSections[2].sectionTitle ?? "");
- setS2Text(pageText.pageSections[2].sectionSubtitle ?? "");
- setS3Subtitle(pageText.pageSections[3].sectionTitle ?? "");
- setS3Text(pageText.pageSections[3].sectionSubtitle ?? "");
- } else {
- alert(response.error);
- }
+ if (response.success) setPageMap(generatePageMap(response.data));
+ else throw new Error(response.error);
})
.catch((error) => {
alert(error);
});
+ setLoading(false);
}, []);
+ if (loading || !pageMap) {
+ return ;
+ }
+
return (
image.imageURI)}
+ backgroundImageURIs={pageMap.get("Header Image Carousel") as string[]}
header=""
title="About Us"
- description={phSubtitle}
+ description={pageMap.get("Subtitle") as string}
/>
diff --git a/frontend/src/app/(web app)/contact/page.tsx b/frontend/src/app/(web app)/contact/page.tsx
index 25cc4de5..d17b0cdc 100644
--- a/frontend/src/app/(web app)/contact/page.tsx
+++ b/frontend/src/app/(web app)/contact/page.tsx
@@ -1,38 +1,60 @@
+"use client";
+
import Image from "next/image";
-import React from "react";
+import React, { useEffect, useState } from "react";
-import styles from "./page.module.css";
+import { getPageData } from "../../../api/pageeditor";
+import ContactForm from "../../../components/ContactForm";
+import ContactInfoCard from "../../../components/ContactInfoCard";
+import LoadingSpinner from "../../../components/admin/LoadingSpinner";
+import { generatePageMap } from "../../admin/util/pageeditUtil";
-import ContactForm from "@/components/ContactForm";
-import ContactInfoCard from "@/components/ContactInfoCard";
+import styles from "./page.module.css";
export default function Contact() {
+ const [pageMap, setPageMap] = useState>();
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ setLoading(true);
+ getPageData("contact")
+ .then((response) => {
+ if (response.success) setPageMap(generatePageMap(response.data));
+ else throw new Error(response.error);
+ })
+ .catch((error) => {
+ alert(error);
+ });
+ setLoading(false);
+ }, []);
+
+ if (loading || !pageMap) {
+ return ;
+ }
+
return (
{/*Reach Out To Us */}
Reach Out To Us
-
- There are many ways to join us and support our mission. Contact us to find out more
- about volunteering opportunities, fundraising, and more !
-
+
{pageMap.get("Subtitle") as string}
diff --git a/frontend/src/app/(web app)/impact/page.tsx b/frontend/src/app/(web app)/impact/page.tsx
index 5b754237..955309e9 100644
--- a/frontend/src/app/(web app)/impact/page.tsx
+++ b/frontend/src/app/(web app)/impact/page.tsx
@@ -1,60 +1,44 @@
"use client";
+
import React, { useEffect, useState } from "react";
-import { getPageText } from "../../../api/pageeditor";
+import { getPageData } from "../../../api/pageeditor";
+import { generatePageMap } from "../../../app/admin/util/pageeditUtil";
+import BackgroundHeader from "../../../components/BackgroundHeader";
+import WhiteCard from "../../../components/WhiteCard";
+import LoadingSpinner from "../../../components/admin/LoadingSpinner";
import styles from "./page.module.css";
-import { BackgroundImage, BackgroundImagePages, getBackgroundImages } from "@/api/images";
-import BackgroundHeader from "@/components/BackgroundHeader";
-import WhiteCard from "@/components/WhiteCard";
-
export default function Impact() {
- const [images, setImages] = useState
([]);
-
- //admin variables
- const [phSubtitle, setPhSubtitle] = useState("");
- const [s1Subtitle, setS1Subtitle] = useState("");
- const [s2Subtitle, setS2Subtitle] = useState("");
+ const [pageMap, setPageMap] = useState>();
+ const [loading, setLoading] = useState(false);
useEffect(() => {
- getBackgroundImages(BackgroundImagePages.TEAM)
- .then((result) => {
- if (result.success) {
- setImages(result.data);
- }
- })
- .catch((error) => {
- alert(error);
- });
- }, []);
-
- let pageText;
- useEffect(() => {
- getPageText("Our Impact")
+ setLoading(true);
+ getPageData("impact")
.then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Subtitle(pageText.pageSections[1].sectionTitle ?? "");
- setS2Subtitle(pageText.pageSections[2].sectionTitle ?? "");
- } else {
- alert(response.error);
- }
+ if (response.success) setPageMap(generatePageMap(response.data));
+ else throw new Error(response.error);
})
.catch((error) => {
alert(error);
});
+ setLoading(false);
}, []);
+ if (loading || !pageMap) {
+ return ;
+ }
+
return (
image.imageURI)}
+ backgroundImageURIs={pageMap.get("Header Image Carousel") as string[]}
header=""
title="Our Impact"
- description={phSubtitle}
+ description={pageMap.get("Subtitle") as string}
/>
@@ -65,14 +49,14 @@ export default function Impact() {
buttonUrl="/testimonials"
buttonText="Learn More"
title="Testimonals"
- description={s1Subtitle}
+ description={pageMap.get("Testimonials Subtitle") as string}
/>
diff --git a/frontend/src/app/(web app)/involved/page.tsx b/frontend/src/app/(web app)/involved/page.tsx
index 04b113ce..06ffc25b 100644
--- a/frontend/src/app/(web app)/involved/page.tsx
+++ b/frontend/src/app/(web app)/involved/page.tsx
@@ -1,58 +1,44 @@
"use client";
+
import React, { useEffect, useState } from "react";
-import { getPageText } from "../../../api/pageeditor";
+import { getPageData } from "../../../api/pageeditor";
+import { generatePageMap } from "../../../app/admin/util/pageeditUtil";
+import BackgroundHeader from "../../../components/BackgroundHeader";
+import WhiteCard from "../../../components/WhiteCard";
+import LoadingSpinner from "../../../components/admin/LoadingSpinner";
import styles from "./page.module.css";
-import { BackgroundImage, BackgroundImagePages, getBackgroundImages } from "@/api/images";
-import BackgroundHeader from "@/components/BackgroundHeader";
-import WhiteCard from "@/components/WhiteCard";
-
export default function Involved() {
- const [images, setImages] = useState
([]);
- const [phSubtitle, setPhSubtitle] = useState("");
- const [s1Subtitle, setS1Subtitle] = useState("");
- const [s2Subtitle, setS2Subtitle] = useState("");
-
- useEffect(() => {
- getBackgroundImages(BackgroundImagePages.TEAM)
- .then((result) => {
- if (result.success) {
- setImages(result.data);
- }
- })
- .catch((error) => {
- alert(error);
- });
- }, []);
+ const [pageMap, setPageMap] = useState>();
+ const [loading, setLoading] = useState(false);
- let pageText;
useEffect(() => {
- getPageText("Get Involved")
+ setLoading(true);
+ getPageData("involved")
.then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Subtitle(pageText.pageSections[1].sectionSubtitle ?? "");
- setS2Subtitle(pageText.pageSections[2].sectionSubtitle ?? "");
- } else {
- alert(response.error);
- }
+ if (response.success) setPageMap(generatePageMap(response.data));
+ else throw new Error(response.error);
})
.catch((error) => {
alert(error);
});
+ setLoading(false);
}, []);
+ if (loading || !pageMap) {
+ return ;
+ }
+
return (
image.imageURI)}
+ backgroundImageURIs={pageMap.get("Header Image Carousel") as string[]}
header=""
title="Get Involved"
- description={phSubtitle}
+ description={pageMap.get("Subtitle") as string}
/>
@@ -63,14 +49,14 @@ export default function Involved() {
buttonUrl="/upcoming-events"
buttonText="Learn More"
title="Upcoming Events"
- description={s1Subtitle}
+ description={pageMap.get("Events Subtitle") as string}
/>
diff --git a/frontend/src/app/(web app)/mission/page.module.css b/frontend/src/app/(web app)/mission/page.module.css
index a5bbcfd4..2908e656 100644
--- a/frontend/src/app/(web app)/mission/page.module.css
+++ b/frontend/src/app/(web app)/mission/page.module.css
@@ -74,7 +74,9 @@
/*For the images*/
.imageContainer {
+ margin-top: -64px;
display: flex;
+ gap: 32px;
flex-direction: column;
}
@@ -87,6 +89,7 @@
}
.storyContainer {
+ margin-top: 24px;
display: flex;
flex-direction: row;
justify-content: space-between;
diff --git a/frontend/src/app/(web app)/mission/page.tsx b/frontend/src/app/(web app)/mission/page.tsx
index f7e37705..e062a2c2 100644
--- a/frontend/src/app/(web app)/mission/page.tsx
+++ b/frontend/src/app/(web app)/mission/page.tsx
@@ -1,96 +1,66 @@
"use client";
-import Image from "next/image";
import React, { useEffect, useState } from "react";
-import { getPageText } from "../../../api/pageeditor";
+import { getPageData } from "../../../api/pageeditor";
+import BackgroundHeader from "../../../components/BackgroundHeader";
import Button from "../../../components/Button";
import ValueCard from "../../../components/ValueCard";
+import LoadingSpinner from "../../../components/admin/LoadingSpinner";
+import { generatePageMap } from "../../admin/util/pageeditUtil";
import styles from "./page.module.css";
-import { BackgroundImage, BackgroundImagePages, getBackgroundImages } from "@/api/images";
-import BackgroundHeader from "@/components/BackgroundHeader";
-
export default function Mission() {
- const [images, setImages] = useState([]);
-
- const [valueSubtitle, setvalueSubtitle] = useState("");
- const [phSubtitle, setPhSubtitle] = useState("");
- const [Value1, setValue1] = useState("");
- const [Value1_Description, setValue1_Description] = useState("");
- const [Value2, setValue2] = useState("");
- const [Value2_Description, setValue2_Description] = useState("");
- const [Value3, setValue3] = useState("");
- const [Value3_Description, setValue3_Description] = useState("");
- const [s1Text, setS1Text] = useState("");
- const [s1Subtitle, setS1Subtitle] = useState("");
+ const [pageMap, setPageMap] = useState>();
+ const [loading, setLoading] = useState(false);
useEffect(() => {
- getBackgroundImages(BackgroundImagePages.TEAM)
- .then((result) => {
- if (result.success) {
- setImages(result.data);
- }
- })
- .catch((error) => {
- alert(error);
- });
- }, []);
-
- let pageText;
- useEffect(() => {
- getPageText("Our Mission")
+ setLoading(true);
+ getPageData("mission")
.then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setvalueSubtitle(pageText.pageSections[1].subtitle ?? "");
- setValue1(pageText.pageSections[2].sectionTitle ?? "");
- setValue1_Description(pageText.pageSections[2].sectionSubtitle ?? "");
- setValue2(pageText.pageSections[3].sectionTitle ?? "");
- setValue2_Description(pageText.pageSections[3].sectionSubtitle ?? "");
- setValue3(pageText.pageSections[4].sectionTitle ?? "");
- setValue3_Description(pageText.pageSections[4].sectionSubtitle ?? "");
- setS1Subtitle(pageText.pageSections[5].sectionTitle ?? "");
- setS1Text(pageText.pageSections[5].sectionSubtitle ?? "");
- } else {
- alert(response.error);
- }
+ if (response.success) setPageMap(generatePageMap(response.data));
+ else throw new Error(response.error);
})
.catch((error) => {
alert(error);
});
+ setLoading(false);
}, []);
+ if (loading || !pageMap) {
+ return ;
+ }
+ console.log((pageMap.get("Image Gallery") as string[])[0]);
+
return (
image.imageURI)}
+ backgroundImageURIs={pageMap.get("Header Image Carousel") as string[]}
header="OUR MISSION"
title="Why We Do It"
- description={phSubtitle}
+ description={pageMap.get("Subtitle") as string}
/>
{/* We pay it forward*/}
-
{valueSubtitle}
+
{pageMap.get("Values Section Title") as string}
@@ -98,41 +68,40 @@ export default function Mission() {
{/* OUR STORY*/}
-
{s1Subtitle}
-
{s1Text}
+
{pageMap.get("Story Section Title") as string}
+
{pageMap.get("Body Text") as string}
diff --git a/frontend/src/app/(web app)/newsletter/page.tsx b/frontend/src/app/(web app)/newsletter/page.tsx
index e41aef69..aa8914eb 100644
--- a/frontend/src/app/(web app)/newsletter/page.tsx
+++ b/frontend/src/app/(web app)/newsletter/page.tsx
@@ -1,43 +1,48 @@
"use client";
+
import React, { useEffect, useState } from "react";
-import { getPageText } from "../../../api/pageeditor";
+import { Newsletter, getAllNewsletters } from "../../../api/newsletter";
+import { getPageData } from "../../../api/pageeditor";
+import BackgroundHeader from "../../../components/BackgroundHeader";
+import Button from "../../../components/Button";
import NewsletterArchive from "../../../components/NewsletterArchive";
import NewsletterCard from "../../../components/NewsletterCard";
import NewsletterPopup from "../../../components/NewsletterPopup";
+import LoadingSpinner from "../../../components/admin/LoadingSpinner";
+import { generatePageMap } from "../../admin/util/pageeditUtil";
import styles from "./page.module.css";
-import { BackgroundImage, BackgroundImagePages, getBackgroundImages } from "@/api/images";
-import { Newsletter, getAllNewsletters } from "@/api/newsletter";
-import BackgroundHeader from "@/components/BackgroundHeader";
-import Button from "@/components/Button";
-
export default function NewsletterPage() {
const [popupOpen, setPopup] = useState(false);
- const [images, setImages] = useState([]);
const [curNewsletters, setCurNewsletters] = useState([]);
const [archiveNewsletters, setArchiveNewsletters] = useState>({});
const [sortedArchives, setSortedArchives] = useState([]);
- //admin variables
- const [phSubtitle, setPhSubtitle] = useState("");
- const [s1Subtitle, setS1Subtitle] = useState("");
- const [s1Text, setS1Text] = useState("");
+ // admin variables
+ const [pageMap, setPageMap] = useState>();
+ const [loading, setLoading] = useState(false);
useEffect(() => {
- getBackgroundImages(BackgroundImagePages.TEAM)
- .then((result) => {
- if (result.success) {
- setImages(result.data);
- }
+ setLoading(true);
+ loadNewspapers();
+ loadPageMap();
+ setLoading(false);
+ }, []);
+
+ function loadPageMap() {
+ getPageData("newsletter")
+ .then((response) => {
+ if (response.success) setPageMap(generatePageMap(response.data));
+ else throw new Error(response.error);
})
.catch((error) => {
alert(error);
});
- }, []);
+ }
- useEffect(() => {
+ function loadNewspapers() {
getAllNewsletters()
.then((response) => {
if (response.success) {
@@ -75,42 +80,28 @@ export default function NewsletterPage() {
.catch((error) => {
alert(error);
});
- }, []);
+ }
const handleSubscribeClick = () => {
setPopup(true);
};
- let pageText;
- useEffect(() => {
- getPageText("Newsletter")
- .then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Subtitle(pageText.pageSections[1].sectionTitle ?? "");
- setS1Text(pageText.pageSections[1].sectionSubtitle ?? "");
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- }, []);
+ if (loading || !pageMap) {
+ return ;
+ }
return (
image.imageURI)}
+ backgroundImageURIs={pageMap.get("Header Image Carousel") as string[]}
header="OUR IMPACT"
title="Newsletter"
- description={phSubtitle}
+ description={pageMap.get("Subtitle") as string}
/>
-
{s1Subtitle}
+
{pageMap.get("Section Title") as string}
-
{s1Text}
+
{pageMap.get("Section Subtitle") as string}
diff --git a/frontend/src/app/(web app)/page.tsx b/frontend/src/app/(web app)/page.tsx
index c4d8e11c..91bf8a4e 100644
--- a/frontend/src/app/(web app)/page.tsx
+++ b/frontend/src/app/(web app)/page.tsx
@@ -1,75 +1,49 @@
"use client";
+
import React, { useEffect, useState } from "react";
-import "../globals.css";
-import { getPageText } from "../../api/pageeditor";
+import { getPageData } from "../../api/pageeditor";
import BackgroundHeader from "../../components/BackgroundHeader";
+import Button from "../../components/Button";
+import Description from "../../components/Description";
+import EventsList from "../../components/EventsList";
+import WhiteCard from "../../components/WhiteCard";
+import LoadingSpinner from "../../components/admin/LoadingSpinner";
+import { generatePageMap } from "../admin/util/pageeditUtil";
import styles from "./page.module.css";
-
-import { BackgroundImage, BackgroundImagePages, getBackgroundImages } from "@/api/images";
-import Button from "@/components/Button";
-import Description from "@/components/Description";
-import EventsList from "@/components/EventsList";
-import WhiteCard from "@/components/WhiteCard";
+import "../globals.css";
export default function Home() {
- const [images, setImages] = useState
([]);
- const [phSubtitle, setPhSubtitle] = useState("");
- const [s1Subtitle, setS1Subtitle] = useState("");
- const [s1Text, setS1Text] = useState("");
- const [s2Subtitle, setS2Subtitle] = useState("");
- const [s2Text, setS2Text] = useState("");
-
- const see_more_text = "See More";
- const sponsor_us_text = "Sponsor Us";
+ const [pageMap, setPageMap] = useState>();
+ const [loading, setLoading] = useState(false);
useEffect(() => {
- getBackgroundImages(BackgroundImagePages.HOME)
- .then((result) => {
- if (result.success) {
- console.log(result.data, "images");
- setImages(result.data);
- }
- })
- .catch((error) => {
- alert(error);
- });
- }, []);
-
- /* Get page data from MongoDB */
- let pageText;
- useEffect(() => {
- getPageText("Home")
+ setLoading(true);
+ getPageData("home")
.then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Subtitle(pageText.pageSections[1].sectionTitle ?? "");
- setS1Text(pageText.pageSections[1].sectionSubtitle ?? "");
- setS2Subtitle(pageText.pageSections[2].sectionTitle ?? "");
- setS2Text(pageText.pageSections[2].sectionSubtitle ?? "");
- console.log("response.data: ", response.data);
- } else {
- alert(response.error);
- }
+ if (response.success) setPageMap(generatePageMap(response.data));
+ else throw new Error(response.error);
})
.catch((error) => {
alert(error);
});
+ setLoading(false);
}, []);
+ if (loading || !pageMap) {
+ return ;
+ }
+
return (
- {images.length > 0 && (
- image.imageURI)}
- header={""}
- title={"4 Future Leaders of Tomorrow"}
- description={phSubtitle}
- button={ }
- />
- )}
+ }
+ />
-
+
-
+
+
+
+
+
+ {(pageMap.get("Sponsor Image Gallery") as string[]).map((url) => (
+
+
+
+ ))}
+
-
-
-
+
diff --git a/frontend/src/app/(web app)/upcoming-events/page.tsx b/frontend/src/app/(web app)/upcoming-events/page.tsx
index e7388d7b..3f81df9e 100644
--- a/frontend/src/app/(web app)/upcoming-events/page.tsx
+++ b/frontend/src/app/(web app)/upcoming-events/page.tsx
@@ -1,63 +1,51 @@
"use client";
+
import React, { useEffect, useState } from "react";
-import { getPageText } from "../../../api/pageeditor";
+import { getPageData } from "../../../api/pageeditor";
+import { generatePageMap } from "../../../app/admin/util/pageeditUtil";
+import BackgroundHeader from "../../../components/BackgroundHeader";
import EventsList from "../../../components/EventsList";
+import LoadingSpinner from "../../../components/admin/LoadingSpinner";
import styles from "./page.module.css";
-import { BackgroundImage, BackgroundImagePages, getBackgroundImages } from "@/api/images";
-import BackgroundHeader from "@/components/BackgroundHeader";
-
export default function UpcomingEvents() {
- const [images, setImages] = useState([]);
- const [phSubtitle, setPhSubtitle] = useState("");
- const [s1Subtitle, setS1Subtitle] = useState("");
- const [s1Text, setS1Text] = useState("");
-
- useEffect(() => {
- getBackgroundImages(BackgroundImagePages.TEAM)
- .then((result) => {
- if (result.success) {
- setImages(result.data);
- }
- })
- .catch((error) => {
- alert(error);
- });
- }, []);
+ const [pageMap, setPageMap] = useState>();
+ const [loading, setLoading] = useState(false);
- let pageText;
useEffect(() => {
- getPageText("Upcoming Events")
+ setLoading(true);
+ getPageData("events")
.then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Subtitle(pageText.pageSections[1].sectionTitle ?? "");
- setS1Text(pageText.pageSections[1].sectionSubtitle ?? "");
- } else {
- alert(response.error);
- }
+ if (response.success) setPageMap(generatePageMap(response.data));
+ else throw new Error(response.error);
})
.catch((error) => {
alert(error);
});
+ setLoading(false);
}, []);
+ if (loading || !pageMap) {
+ return ;
+ }
+
return (
image.imageURI)}
+ backgroundImageURIs={pageMap.get("Header Image Carousel") as string[]}
header="GET INVOLVED"
title="Upcoming Events"
- description={phSubtitle}
+ description={pageMap.get("Subtitle") as string}
/>
-
{s1Subtitle}
-
{s1Text}
+
{pageMap.get("Section Title") as string}
+
+ {pageMap.get("Section Subtitle") as string}
+
From 5a7ccf28af093abf0501f76350b563c8993a3b95 Mon Sep 17 00:00:00 2001
From: Jack Hansen <47556286+jackavh@users.noreply.github.com>
Date: Wed, 29 May 2024 14:52:03 -0700
Subject: [PATCH 07/14] refactor page editors to use new image components
---
.../admin/page-editor/about/AboutEditor.tsx | 106 +++++++++
.../src/app/admin/page-editor/about/page.tsx | 193 +----------------
.../page-editor/contact/ContactEditor.tsx | 90 ++++++++
.../app/admin/page-editor/contact/page.tsx | 77 +------
.../events/UpcomingEventsEditor.tsx | 92 ++++++++
.../src/app/admin/page-editor/events/page.tsx | 165 +-------------
.../app/admin/page-editor/home/HomeEditor.tsx | 98 +++++++++
.../src/app/admin/page-editor/home/page.tsx | 161 +-------------
.../admin/page-editor/impact/ImpactEditor.tsx | 95 ++++++++
.../src/app/admin/page-editor/impact/page.tsx | 142 +-----------
.../page-editor/involved/InvolvedEditor.tsx | 93 ++++++++
.../app/admin/page-editor/involved/page.tsx | 174 +--------------
.../page-editor/mission/MissionEditor.tsx | 110 ++++++++++
.../app/admin/page-editor/mission/page.tsx | 203 +-----------------
.../newsletter/NewsletterEditor.tsx | 93 ++++++++
.../app/admin/page-editor/newsletter/page.tsx | 130 +----------
.../src/app/admin/page-editor/page.module.css | 2 +-
frontend/src/app/admin/page-editor/page.tsx | 2 +-
.../src/app/admin/page-editor/team/page.tsx | 10 +-
19 files changed, 854 insertions(+), 1182 deletions(-)
create mode 100644 frontend/src/app/admin/page-editor/about/AboutEditor.tsx
create mode 100644 frontend/src/app/admin/page-editor/contact/ContactEditor.tsx
create mode 100644 frontend/src/app/admin/page-editor/events/UpcomingEventsEditor.tsx
create mode 100644 frontend/src/app/admin/page-editor/home/HomeEditor.tsx
create mode 100644 frontend/src/app/admin/page-editor/impact/ImpactEditor.tsx
create mode 100644 frontend/src/app/admin/page-editor/involved/InvolvedEditor.tsx
create mode 100644 frontend/src/app/admin/page-editor/mission/MissionEditor.tsx
create mode 100644 frontend/src/app/admin/page-editor/newsletter/NewsletterEditor.tsx
diff --git a/frontend/src/app/admin/page-editor/about/AboutEditor.tsx b/frontend/src/app/admin/page-editor/about/AboutEditor.tsx
new file mode 100644
index 00000000..cfcc38cb
--- /dev/null
+++ b/frontend/src/app/admin/page-editor/about/AboutEditor.tsx
@@ -0,0 +1,106 @@
+import React, { useEffect, useState } from "react";
+
+import { getPageData, updatePageData } from "../../../../api/pageeditor";
+import Toast from "../../../../components/admin/Toast";
+import CancelButton from "../../../../components/admin/pageeditor/CancelButton";
+import { CollapsibleFields } from "../../../../components/admin/pageeditor/CollapsibleFields";
+import { usePage, usePageDispatch } from "../../../../components/admin/pageeditor/PageProvider";
+import SaveButton from "../../../../components/admin/pageeditor/SaveButton";
+
+import styles from "./page.module.css";
+
+export default function AboutEditor() {
+ const page = usePage();
+ const dispatch = usePageDispatch();
+ const [open, setOpen] = useState(false);
+
+ // Send page data from MongoDB to provider state
+ const updatePageStateFromDB = () => {
+ getPageData(page.name)
+ .then((response) => {
+ if (response.success) {
+ dispatch({
+ type: "edit_page",
+ setIsEdited: false,
+ page: {
+ ...page,
+ fields: response.data.fields,
+ },
+ });
+ } else {
+ alert("Could not connect to database. Any changes you make will not take effect!!!");
+ }
+ })
+ .catch(console.error);
+ };
+
+ // When the page loads update state to reflect page data from MongoDB
+ useEffect(() => {
+ updatePageStateFromDB();
+ }, []);
+
+ // Implement save logic
+ const handleSave = () => {
+ if (page.isEdited) {
+ // set isEdited to false or else when we load from mongo it will have wrong state
+ updatePageData(page.name, { ...page, isEdited: false })
+ .then(() => {
+ setOpen(true);
+ })
+ .catch((error) => {
+ alert(error);
+ });
+ // make sure to reflect isEdited false on local state
+ dispatch({
+ type: "set_isEdited",
+ setIsEdited: false,
+ });
+ }
+ };
+
+ // Implement cancel logic
+ const handleCancel = () => {
+ if (page.isEdited) {
+ updatePageStateFromDB();
+ dispatch({
+ type: "set_isEdited",
+ setIsEdited: false,
+ });
+ }
+ };
+
+ return (
+
+
{
+ setOpen(false);
+ }}
+ />
+
+
+ );
+}
diff --git a/frontend/src/app/admin/page-editor/about/page.tsx b/frontend/src/app/admin/page-editor/about/page.tsx
index 826ce2c5..446381fd 100644
--- a/frontend/src/app/admin/page-editor/about/page.tsx
+++ b/frontend/src/app/admin/page-editor/about/page.tsx
@@ -1,200 +1,25 @@
"use client";
-import React, { useEffect, useState } from "react";
-import { getPageText, updatePage } from "../../../../api/pageeditor";
+import { Page } from "../../../../api/pageeditor";
+import PageToggle from "../../../../components/PageToggle";
+import { PageProvider } from "../../../../components/admin/pageeditor/PageProvider";
+import defaultPage from "../../../../components/admin/pageeditor/defaultPages/aboutPageDefault.json";
+import AboutEditor from "./AboutEditor";
import styles from "./page.module.css";
-import AlertBanner from "@/components/AlertBanner";
-import Button from "@/components/Button";
-import CancelButton from "@/components/CancelButton";
-import Collapsable, { UploadImageTypes } from "@/components/Collapsable";
-import PageToggle from "@/components/PageToggle";
-
-export default function AboutEditor() {
- const [isEdited, setIsEdited] = useState(false);
- const [phSubtitle, setPhSubtitle] = useState("");
- const [s1Subtitle, setS1Subtitle] = useState("");
- const [s1Text, setS1Text] = useState("");
- const [s2Subtitle, setS2Subtitle] = useState("");
- const [s2Text, setS2Text] = useState("");
- const [s3Subtitle, setS3Subtitle] = useState("");
- const [s3Text, setS3Text] = useState("");
- const [missionImages, setMissionImages] = useState([]);
-const [teamImages, setTeamImages] = useState([]);
-const [contactImages, setContactImages] = useState([]);
-
- const [showAlert, setShowAlert] = useState(false);
-
- /* Get page data from MongoDB */
- let pageText;
- useEffect(() => {
- getPageText("About Us")
- .then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Subtitle(pageText.pageSections[1].sectionTitle ?? "");
- setS1Text(pageText.pageSections[1].sectionSubtitle ?? "");
- setS2Subtitle(pageText.pageSections[2].sectionTitle ?? "");
- setS2Text(pageText.pageSections[2].sectionSubtitle ?? "");
- setS3Subtitle(pageText.pageSections[3].sectionTitle ?? "");
- setS3Text(pageText.pageSections[3].sectionSubtitle ?? "");
- console.log("response.data: ", response.data);
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- }, []);
-
- /* Handle Fields upon edit */
- const handleEdit = (event: React.ChangeEvent) => {
- setIsEdited(true);
- if(event.target){
- if (event.target.id === "Page Subtitle: Subtitle") {
- setPhSubtitle(event.target.value);
- } else if (event.target.id === "Section 1 - Our Mission: Section Title") {
- setS1Subtitle(event.target.value);
- } else if (event.target.id === "Section 1 - Our Mission: Body Text") {
- setS1Text(event.target.value);
- } else if (event.target.id === "Section 2 - Our Team: Section Title") {
- setS2Subtitle(event.target.value);
- } else if (event.target.id === "Section 2 - Our Team: Body Text") {
- setS2Text(event.target.value);
- } else if (event.target.id === "Section 3 - Contact Us: Section Title") {
- setS3Subtitle(event.target.value);
- } else if (event.target.id === "Section 3 - Contact Us: Body Text") {
- setS3Text(event.target.value);
- }
- }
- };
-
- const handleSave = () => {
- // Implement save logic
- if (isEdited) {
- console.log("Save changes");
- updatePage({
- //Pass edited text to MongoDB
- page: "About Us",
- pageSections: [
- {
- subtitle: phSubtitle,
- },
- {
- sectionTitle: s1Subtitle,
- sectionSubtitle: s1Text,
- },
- {
- sectionTitle: s2Subtitle,
- sectionSubtitle: s2Text,
- },
- {
- sectionTitle: s3Subtitle,
- sectionSubtitle: s3Text,
- },
- ],
- })
- .then((response) => {
- if (response.success) {
- alert("Success!");
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- setIsEdited(false);
- }
- };
-
- const handleCancel = () => {
- // Implement cancel logic
- if (isEdited) {
- console.log("Cancel changes");
- getPageText("About Us")
- .then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Subtitle(pageText.pageSections[1].sectionTitle ?? "");
- setS1Text(pageText.pageSections[1].sectionSubtitle ?? "");
- setS2Subtitle(pageText.pageSections[2].sectionTitle ?? "");
- setS2Text(pageText.pageSections[2].sectionSubtitle ?? "");
- setS3Subtitle(pageText.pageSections[3].sectionTitle ?? "");
- setS3Text(pageText.pageSections[3].sectionSubtitle ?? "");
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- setIsEdited(false);
- }
- };
-
- const handleCloseAlert = () => {
- setShowAlert(false);
- };
-
+export default function AboutEditorPage() {
return (
-
-
+
+
+
);
}
diff --git a/frontend/src/app/admin/page-editor/contact/ContactEditor.tsx b/frontend/src/app/admin/page-editor/contact/ContactEditor.tsx
new file mode 100644
index 00000000..bafff5b4
--- /dev/null
+++ b/frontend/src/app/admin/page-editor/contact/ContactEditor.tsx
@@ -0,0 +1,90 @@
+import React, { useEffect, useState } from "react";
+
+import { getPageData, updatePageData } from "../../../../api/pageeditor";
+import Toast from "../../../../components/admin/Toast";
+import CancelButton from "../../../../components/admin/pageeditor/CancelButton";
+import { CollapsibleFields } from "../../../../components/admin/pageeditor/CollapsibleFields";
+import { usePage, usePageDispatch } from "../../../../components/admin/pageeditor/PageProvider";
+import SaveButton from "../../../../components/admin/pageeditor/SaveButton";
+
+import styles from "./page.module.css";
+
+export default function MissionEditor() {
+ const page = usePage();
+ const dispatch = usePageDispatch();
+ const [open, setOpen] = useState(false);
+
+ // Send page data from MongoDB to provider state
+ const updatePageStateFromDB = () => {
+ getPageData(page.name)
+ .then((response) => {
+ if (response.success) {
+ dispatch({
+ type: "edit_page",
+ setIsEdited: false,
+ page: {
+ ...page,
+ fields: response.data.fields,
+ },
+ });
+ } else {
+ alert("Could not connect to database. Any changes you make will not take effect!!!");
+ }
+ })
+ .catch((error) => {
+ alert(error);
+ });
+ };
+
+ // When the page loads update state to reflect page data from MongoDB
+ useEffect(() => {
+ updatePageStateFromDB();
+ }, []);
+
+ // Implement save logic
+ const handleSave = () => {
+ if (page.isEdited) {
+ // set isEdited to false or else when we load from mongo it will have wrong state
+ updatePageData(page.name, { ...page, isEdited: false })
+ .then(() => {
+ setOpen(true);
+ })
+ .catch((error) => {
+ alert(error);
+ });
+ dispatch({
+ type: "set_isEdited",
+ setIsEdited: false,
+ });
+ }
+ };
+
+ // Implement cancel logic
+ const handleCancel = () => {
+ if (page.isEdited) {
+ updatePageStateFromDB();
+ dispatch({
+ type: "set_isEdited",
+ setIsEdited: false,
+ });
+ }
+ };
+
+ return (
+
+
{
+ setOpen(false);
+ }}
+ />
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/app/admin/page-editor/contact/page.tsx b/frontend/src/app/admin/page-editor/contact/page.tsx
index 840a3b29..911b33c5 100644
--- a/frontend/src/app/admin/page-editor/contact/page.tsx
+++ b/frontend/src/app/admin/page-editor/contact/page.tsx
@@ -1,32 +1,13 @@
"use client";
-import React, { useState } from "react";
-import styles from "./page.module.css";
-
-import Button from "@/components/Button";
-import CancelButton from "@/components/CancelButton";
-import Collapsable from "@/components/Collapsable";
-import PageToggle from "@/components/PageToggle";
-
-export default function ContactEditor() {
- const [isEdited, setIsEdited] = useState(false);
-
- const handleEdit = () => {
- setIsEdited(true);
- };
+import PageToggle from "../../../../components/PageToggle";
+import { PageProvider } from "../../../../components/admin/pageeditor/PageProvider";
+import defaultPage from "../../../../components/admin/pageeditor/defaultPages/contactPageDefault.json";
- const handleSave = () => {
- // Implement save logic
- console.log("Save changes");
- setIsEdited(false);
- };
-
- const handleCancel = () => {
- // Implement cancel logic
- console.log("Cancel changes");
- setIsEdited(false);
- };
+import ContactEditor from "./ContactEditor";
+import styles from "./page.module.css";
+export default function ContactEditorPage() {
return (
-
+
+
+
+
);
}
diff --git a/frontend/src/app/admin/page-editor/events/UpcomingEventsEditor.tsx b/frontend/src/app/admin/page-editor/events/UpcomingEventsEditor.tsx
new file mode 100644
index 00000000..3ddb54bc
--- /dev/null
+++ b/frontend/src/app/admin/page-editor/events/UpcomingEventsEditor.tsx
@@ -0,0 +1,92 @@
+import React, { useEffect, useState } from "react";
+
+import { getPageData, updatePageData } from "../../../../api/pageeditor";
+import Toast from "../../../../components/admin/Toast";
+import CancelButton from "../../../../components/admin/pageeditor/CancelButton";
+import { CollapsibleFields } from "../../../../components/admin/pageeditor/CollapsibleFields";
+import { usePage, usePageDispatch } from "../../../../components/admin/pageeditor/PageProvider";
+import SaveButton from "../../../../components/admin/pageeditor/SaveButton";
+
+import styles from "./page.module.css";
+
+export default function UpcomingEventsEditor() {
+ const page = usePage();
+ const dispatch = usePageDispatch();
+ const [open, setOpen] = useState(false);
+
+ // Send page data from MongoDB to provider state
+ const updatePageStateFromDB = () => {
+ getPageData(page.name)
+ .then((response) => {
+ if (response.success) {
+ dispatch({
+ type: "edit_page",
+ setIsEdited: false,
+ page: {
+ ...page,
+ fields: response.data.fields,
+ },
+ });
+ } else {
+ alert("Could not connect to database. Any changes you make will not take effect!!!");
+ }
+ })
+ .catch((error) => {
+ alert(error);
+ });
+ };
+
+ // When the page loads update state to reflect page data from MongoDB
+ useEffect(() => {
+ updatePageStateFromDB();
+ }, []);
+
+ // Implement save logic
+ const handleSave = () => {
+ if (page.isEdited) {
+ // set isEdited to false or else when we load from mongo it will have wrong state
+ updatePageData(page.name, { ...page, isEdited: false })
+ .then(() => {
+ setOpen(true);
+ })
+ .catch((error) => {
+ alert(error);
+ });
+ dispatch({
+ type: "set_isEdited",
+ setIsEdited: false,
+ });
+ }
+ };
+
+ // Implement cancel logic
+ const handleCancel = () => {
+ if (page.isEdited) {
+ updatePageStateFromDB();
+ dispatch({
+ type: "set_isEdited",
+ setIsEdited: false,
+ });
+ }
+ };
+
+ return (
+
+
{
+ setOpen(false);
+ }}
+ />
+
+
+ );
+}
diff --git a/frontend/src/app/admin/page-editor/events/page.tsx b/frontend/src/app/admin/page-editor/events/page.tsx
index a6300956..68ddda5a 100644
--- a/frontend/src/app/admin/page-editor/events/page.tsx
+++ b/frontend/src/app/admin/page-editor/events/page.tsx
@@ -1,173 +1,24 @@
"use client";
-import React, { useEffect, useState } from "react";
-import { getPageText, updatePage } from "../../../../api/pageeditor";
+import PageToggle from "../../../../components/PageToggle";
+import { PageProvider } from "../../../../components/admin/pageeditor/PageProvider";
+import defaultPage from "../../../../components/admin/pageeditor/defaultPages/eventsPageDefault.json";
import styles from "./page.module.css";
-import AlertBanner from "@/components/AlertBanner";
-import Button from "@/components/Button";
-import CancelButton from "@/components/CancelButton";
-import Collapsable from "@/components/Collapsable";
-import PageToggle from "@/components/PageToggle";
-import { WarningModule } from "@/components/WarningModule";
-
-export default function EventsEditor() {
- const [isEdited, setIsEdited] = useState(false);
- const [phSubtitle, setPhSubtitle] = useState("");
- const [s1Title, setS1Title] = useState("");
- const [s1Text, setS1Text] = useState("");
-
- const [showAlert, setShowAlert] = useState(false);
- const [warningOpen, setWarningOpen] = useState(false);
-
- /* Get page data from MongoDB */
- let pageText;
- useEffect(() => {
- getPageText("Upcoming Events")
- .then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Title(pageText.pageSections[1].sectionTitle ?? "");
- setS1Text(pageText.pageSections[1].sectionSubtitle ?? "");
- console.log("response.data: ", response.data);
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- }, []);
-
- /* Handle Fields upon edit */
- const handleEdit = (event: React.ChangeEvent) => {
- setIsEdited(true);
- if (event.target.id === "Page Header: Subtitle") {
- setPhSubtitle(event.target.value);
- } else if (event.target.id === "Section 1: Section Title") {
- setS1Title(event.target.value);
- } else if (event.target.id === "Section 1: Section Subtitle") {
- setS1Text(event.target.value);
- }
- };
-
- const handleSave = () => {
- // Implement save logic
- if (isEdited) {
- console.log("Save changes");
- updatePage({
- //Pass edited text to MongoDB
- page: "Upcoming Events",
- pageSections: [
- {
- subtitle: phSubtitle,
- },
- {
- sectionTitle: s1Title,
- sectionSubtitle: s1Text,
- },
- ],
- })
- .then((response) => {
- if (response.success) {
- setShowAlert(true);
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- setIsEdited(false);
- }
- setWarningOpen(false);
- };
-
- const handleCancel = () => {
- // Show cancel warning
- if (isEdited) {
- setWarningOpen(true);
- }
- };
-
- const confirmCancel = () => {
- // Implement cancel logic
- setWarningOpen(false);
- console.log("Cancel changes");
- getPageText("Upcoming Events")
- .then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Title(pageText.pageSections[1].sectionTitle ?? "");
- setS1Text(pageText.pageSections[1].sectionSubtitle ?? "");
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- setIsEdited(false);
- };
-
- const handleCloseAlert = () => {
- setShowAlert(false);
- };
-
+export default function UpcomingEventEditorPage() {
return (
-
-
- {warningOpen &&
}
-
- {warningOpen && (
- {
- setWarningOpen(false);
- }}
- />
- )}
-
-
-
+
+
+
+
);
}
diff --git a/frontend/src/app/admin/page-editor/home/HomeEditor.tsx b/frontend/src/app/admin/page-editor/home/HomeEditor.tsx
new file mode 100644
index 00000000..7711e9aa
--- /dev/null
+++ b/frontend/src/app/admin/page-editor/home/HomeEditor.tsx
@@ -0,0 +1,98 @@
+import React, { useEffect, useState } from "react";
+
+import { getPageData, updatePageData } from "../../../../api/pageeditor";
+import Toast from "../../../../components/admin/Toast";
+import CancelButton from "../../../../components/admin/pageeditor/CancelButton";
+import { CollapsibleFields } from "../../../../components/admin/pageeditor/CollapsibleFields";
+import { usePage, usePageDispatch } from "../../../../components/admin/pageeditor/PageProvider";
+import SaveButton from "../../../../components/admin/pageeditor/SaveButton";
+
+import styles from "./page.module.css";
+
+export default function HomeEditor() {
+ const page = usePage();
+ const dispatch = usePageDispatch();
+ const [open, setOpen] = useState(false);
+
+ // Send page data from MongoDB to provider state
+ const updatePageStateFromDB = () => {
+ getPageData(page.name)
+ .then((response) => {
+ if (response.success) {
+ dispatch({
+ type: "edit_page",
+ setIsEdited: false,
+ page: {
+ ...page,
+ fields: response.data.fields,
+ },
+ });
+ } else {
+ alert("Could not connect to database. Any changes you make will not take effect!!!");
+ }
+ })
+ .catch((error) => {
+ alert(error);
+ });
+ };
+
+ // When the page loads update state to reflect page data from MongoDB
+ useEffect(() => {
+ updatePageStateFromDB();
+ }, []);
+
+ // Implement save logic
+ const handleSave = () => {
+ if (page.isEdited) {
+ // set isEdited to false or else when we load from mongo it will have wrong state
+ updatePageData(page.name, { ...page, isEdited: false })
+ .then(() => {
+ setOpen(true);
+ })
+ .catch((error) => {
+ alert(error);
+ });
+ dispatch({
+ type: "set_isEdited",
+ setIsEdited: false,
+ });
+ }
+ };
+
+ // Implement cancel logic
+ const handleCancel = () => {
+ if (page.isEdited) {
+ updatePageStateFromDB();
+ dispatch({
+ type: "set_isEdited",
+ setIsEdited: false,
+ });
+ }
+ };
+
+ return (
+
+
{
+ setOpen(false);
+ }}
+ />
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/app/admin/page-editor/home/page.tsx b/frontend/src/app/admin/page-editor/home/page.tsx
index 1de62588..d05ef1d2 100644
--- a/frontend/src/app/admin/page-editor/home/page.tsx
+++ b/frontend/src/app/admin/page-editor/home/page.tsx
@@ -1,164 +1,19 @@
"use client";
-import React, { useEffect, useState } from "react";
-import { getPageText, updatePage } from "../../../../api/pageeditor";
+import PageToggle from "../../../../components/PageToggle";
+import { PageProvider } from "../../../../components/admin/pageeditor/PageProvider";
+import defaultPage from "../../../../components/admin/pageeditor/defaultPages/homePageDefault.json";
+import HomeEditor from "./HomeEditor";
import styles from "./page.module.css";
-import Button from "@/components/Button";
-import CancelButton from "@/components/CancelButton";
-import Collapsable, { UploadImageTypes } from "@/components/Collapsable";
-import PageToggle from "@/components/PageToggle";
-
-// import PageEditorCard from "@/components/PageEditorCard";
-
-export default function HomeEditor() {
- const [isEdited, setIsEdited] = useState(false);
- const [phSubtitle, setPhSubtitle] = useState("");
- const [s1Subtitle, setS1Subtitle] = useState("");
- const [s1Text, setS1Text] = useState("");
- const [s2Subtitle, setS2Subtitle] = useState("");
- const [s2Text, setS2Text] = useState("");
- const [sponsorImages, setSponsorImages] = useState([]); //state that stores the image urls in the page
-
- /* Get page data from MongoDB */
- //todo: load current image data from mongoDB and store in state
- let pageText;
- useEffect(() => {
- getPageText("Home")
- .then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Subtitle(pageText.pageSections[1].sectionTitle ?? "");
- setS1Text(pageText.pageSections[1].sectionSubtitle ?? "");
- setS2Subtitle(pageText.pageSections[2].sectionTitle ?? "");
- setS2Text(pageText.pageSections[2].sectionSubtitle ?? "");
- //setSponsorImages...
- console.log("response.data: ", response.data);
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- }, []);
-
- /* Handle Fields upon edit */
- const handleEdit = (event: React.ChangeEvent) => {
- setIsEdited(true);
- if(event.target){
- if (event.target.id === "Page Header: Subtitle") {
- setPhSubtitle(event.target.value);
- } else if (event.target.id === "Section 1: Section Title") {
- setS1Subtitle(event.target.value);
- } else if (event.target.id === "Section 1: Body Text") {
- setS1Text(event.target.value);
- } else if (event.target.id === "Section 2: Section Title") {
- setS2Subtitle(event.target.value);
- } else if (event.target.id === "Section 2: Body Text") {
- setS2Text(event.target.value);
- }
- }
- };
-
- const handleSave = () => {
- // Implement save logic
- if (isEdited) {
- console.log("Save changes", sponsorImages);
- updatePage({
- //Pass edited text to MongoDB
- //TODO: update with image handling
- page: "Home",
- pageSections: [
- {
- subtitle: phSubtitle,
- },
- {
- sectionTitle: s1Subtitle,
- sectionSubtitle: s1Text,
- },
- {
- sectionTitle: s2Subtitle,
- sectionSubtitle: s2Text,
- },
- //secitionTitle: sponsorImages...
- ],
- })
- .then((response) => {
- if (response.success) {
- alert("Success!");
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- setIsEdited(false);
- }
- };
-
- const handleCancel = () => {
- // Implement cancel logic
- if (isEdited) {
- console.log("Cancel changes");
- getPageText("Home")
- .then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Subtitle(pageText.pageSections[1].sectionTitle ?? "");
- setS1Text(pageText.pageSections[1].sectionSubtitle ?? "");
- setS2Subtitle(pageText.pageSections[2].sectionTitle ?? "");
- setS2Text(pageText.pageSections[2].sectionSubtitle ?? "");
- //setSponsorImages (refetch and reset to what it used to be)
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- setIsEdited(false);
- }
- };
-
+export default function HomePage() {
return (
-
+
+
+
);
}
diff --git a/frontend/src/app/admin/page-editor/impact/ImpactEditor.tsx b/frontend/src/app/admin/page-editor/impact/ImpactEditor.tsx
new file mode 100644
index 00000000..094b6ea0
--- /dev/null
+++ b/frontend/src/app/admin/page-editor/impact/ImpactEditor.tsx
@@ -0,0 +1,95 @@
+import React, { useEffect, useState } from "react";
+
+import { getPageData, updatePageData } from "../../../../api/pageeditor";
+import Toast from "../../../../components/admin/Toast";
+import CancelButton from "../../../../components/admin/pageeditor/CancelButton";
+import { CollapsibleFields } from "../../../../components/admin/pageeditor/CollapsibleFields";
+import { usePage, usePageDispatch } from "../../../../components/admin/pageeditor/PageProvider";
+import SaveButton from "../../../../components/admin/pageeditor/SaveButton";
+
+import styles from "./page.module.css";
+
+export default function AboutEditor() {
+ const page = usePage();
+ const dispatch = usePageDispatch();
+ const [open, setOpen] = useState(false);
+
+ // Send page data from MongoDB to provider state
+ const updatePageStateFromDB = () => {
+ getPageData(page.name)
+ .then((response) => {
+ if (response.success) {
+ dispatch({
+ type: "edit_page",
+ setIsEdited: false,
+ page: {
+ ...page,
+ fields: response.data.fields,
+ },
+ });
+ } else {
+ alert("Could not connect to database. Any changes you make will not take effect!!!");
+ }
+ })
+ .catch(console.error);
+ };
+
+ // When the page loads update state to reflect page data from MongoDB
+ useEffect(() => {
+ updatePageStateFromDB();
+ }, []);
+
+ // Implement save logic
+ const handleSave = () => {
+ if (page.isEdited) {
+ // set isEdited to false or else when we load from mongo it will have wrong state
+ updatePageData(page.name, { ...page, isEdited: false })
+ .then(() => {
+ setOpen(true);
+ })
+ .catch((error) => {
+ alert(error);
+ });
+ // make sure to reflect isEdited false on local state
+ dispatch({
+ type: "set_isEdited",
+ setIsEdited: false,
+ });
+ }
+ };
+
+ // Implement cancel logic
+ const handleCancel = () => {
+ if (page.isEdited) {
+ updatePageStateFromDB();
+ dispatch({
+ type: "set_isEdited",
+ setIsEdited: false,
+ });
+ }
+ };
+
+ return (
+
+
{
+ setOpen(false);
+ }}
+ />
+
+
+ );
+}
diff --git a/frontend/src/app/admin/page-editor/impact/page.tsx b/frontend/src/app/admin/page-editor/impact/page.tsx
index d04c332d..863ff084 100644
--- a/frontend/src/app/admin/page-editor/impact/page.tsx
+++ b/frontend/src/app/admin/page-editor/impact/page.tsx
@@ -1,112 +1,14 @@
"use client";
-import React, { useEffect, useState } from "react";
-import { getPageText, updatePage } from "../../../../api/pageeditor";
+import { Page } from "../../../../api/pageeditor";
+import PageToggle from "../../../../components/PageToggle";
+import { PageProvider } from "../../../../components/admin/pageeditor/PageProvider";
+import defaultPage from "../../../../components/admin/pageeditor/defaultPages/impactPageDefault.json";
+import ImpactEditor from "./ImpactEditor";
import styles from "./page.module.css";
-import Button from "@/components/Button";
-import CancelButton from "@/components/CancelButton";
-import Collapsable from "@/components/Collapsable";
-import PageToggle from "@/components/PageToggle";
-// import { PhpSharp } from "@mui/icons-material";
-
-// import PageEditorCard from "@/components/PageEditorCard";
-
-export default function ImpactEditor() {
- const [isEdited, setIsEdited] = useState(false);
-
- const [phSubtitle, setPhSubtitle] = useState("");
- const [s1Subtitle, setS1Subtitle] = useState("");
- const [s2Subtitle, setS2Subtitle] = useState("");
-
- /* Get page data from MongoDB */
- let pageText;
- useEffect(() => {
- getPageText("Our Impact")
- .then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Subtitle(pageText.pageSections[1].sectionTitle ?? "");
- setS2Subtitle(pageText.pageSections[2].sectionTitle ?? "");
- console.log("response.data: ", response.data);
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- }, []);
-
- /* Handle Fields upon edit */
- const handleEdit = (event: React.ChangeEvent) => {
- setIsEdited(true);
- if (event.target.id === "Page Header: Subtitle") {
- setPhSubtitle(event.target.value);
- } else if (event.target.id === "Section 1 - Testimonials: Subtitle") {
- setS1Subtitle(event.target.value);
- } else if (event.target.id === "Section 2 - Newsletter: Subtitle") {
- setS2Subtitle(event.target.value);
- }
- };
-
- const handleSave = () => {
- // Implement save logic
- if (isEdited) {
- console.log("Save changes");
- updatePage({
- //Pass edited text to MongoDB
- page: "Our Impact",
- pageSections: [
- {
- subtitle: phSubtitle,
- },
- {
- sectionTitle: s1Subtitle,
- },
- {
- sectionTitle: s2Subtitle,
- },
- ],
- })
- .then((response) => {
- if (response.success) {
- alert("Success!");
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- setIsEdited(false);
- }
- };
-
- const handleCancel = () => {
- // Implement cancel logic
- if (isEdited) {
- console.log("Cancel changes");
- getPageText("Our Impact")
- .then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Subtitle(pageText.pageSections[1].sectionTitle ?? "");
- setS2Subtitle(pageText.pageSections[2].sectionTitle ?? "");
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- setIsEdited(false);
- }
- };
-
+export default function ImpactEditorPage() {
return (
-
+
+
+
);
}
diff --git a/frontend/src/app/admin/page-editor/involved/InvolvedEditor.tsx b/frontend/src/app/admin/page-editor/involved/InvolvedEditor.tsx
new file mode 100644
index 00000000..1e17955e
--- /dev/null
+++ b/frontend/src/app/admin/page-editor/involved/InvolvedEditor.tsx
@@ -0,0 +1,93 @@
+import React, { useEffect, useState } from "react";
+
+import { getPageData, updatePageData } from "../../../../api/pageeditor";
+import Toast from "../../../../components/admin/Toast";
+import CancelButton from "../../../../components/admin/pageeditor/CancelButton";
+import { CollapsibleFields } from "../../../../components/admin/pageeditor/CollapsibleFields";
+import { usePage, usePageDispatch } from "../../../../components/admin/pageeditor/PageProvider";
+import SaveButton from "../../../../components/admin/pageeditor/SaveButton";
+
+import styles from "./page.module.css";
+
+export default function InvolvedEditor() {
+ const page = usePage();
+ const dispatch = usePageDispatch();
+ const [open, setOpen] = useState(false);
+
+ // Send page data from MongoDB to provider state
+ const updatePageStateFromDB = () => {
+ getPageData(page.name)
+ .then((response) => {
+ if (response.success) {
+ dispatch({
+ type: "edit_page",
+ setIsEdited: false,
+ page: {
+ ...page,
+ fields: response.data.fields,
+ },
+ });
+ } else {
+ alert("Could not connect to database. Any changes you make will not take effect!!!");
+ }
+ })
+ .catch((error) => {
+ alert(error);
+ });
+ };
+
+ // When the page loads update state to reflect page data from MongoDB
+ useEffect(() => {
+ updatePageStateFromDB();
+ }, []);
+
+ // Implement save logic
+ const handleSave = () => {
+ if (page.isEdited) {
+ // set isEdited to false or else when we load from mongo it will have wrong state
+ updatePageData(page.name, { ...page, isEdited: false })
+ .then(() => {
+ setOpen(true);
+ })
+ .catch((error) => {
+ alert(error);
+ });
+ dispatch({
+ type: "set_isEdited",
+ setIsEdited: false,
+ });
+ }
+ };
+
+ // Implement cancel logic
+ const handleCancel = () => {
+ if (page.isEdited) {
+ updatePageStateFromDB();
+ dispatch({
+ type: "set_isEdited",
+ setIsEdited: false,
+ });
+ }
+ };
+
+ return (
+
+
{
+ setOpen(false);
+ }}
+ />
+
+
+ );
+}
diff --git a/frontend/src/app/admin/page-editor/involved/page.tsx b/frontend/src/app/admin/page-editor/involved/page.tsx
index 3dba7380..edb3f844 100644
--- a/frontend/src/app/admin/page-editor/involved/page.tsx
+++ b/frontend/src/app/admin/page-editor/involved/page.tsx
@@ -1,181 +1,25 @@
"use client";
-import React, { useEffect, useState } from "react";
-import { getPageText, updatePage } from "../../../../api/pageeditor";
+import PageToggle from "../../../../components/PageToggle";
+import { PageProvider } from "../../../../components/admin/pageeditor/PageProvider";
+import defaultPage from "../../../../components/admin/pageeditor/defaultPages/involvedPageDefault.json";
+import InvolvedEditor from "./InvolvedEditor";
import styles from "./page.module.css";
-import AlertBanner from "@/components/AlertBanner";
-import Button from "@/components/Button";
-import CancelButton from "@/components/CancelButton";
-import Collapsable from "@/components/Collapsable";
-import PageToggle from "@/components/PageToggle";
-import { WarningModule } from "@/components/WarningModule";
-
-export default function InvolvedEditor() {
- const [isEdited, setIsEdited] = useState(false);
- const [phSubtitle, setPhSubtitle] = useState("");
- const [s1Subtitle, setS1Subtitle] = useState("");
- const [s2Subtitle, setS2Subtitle] = useState("");
-
- const [showAlert, setShowAlert] = useState(false);
- const [warningOpen, setWarningOpen] = useState(false);
-
- /* Get page data from MongoDB */
- let pageText;
- useEffect(() => {
- getPageText("Get Involved")
- .then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Subtitle(pageText.pageSections[1].sectionSubtitle ?? "");
- setS2Subtitle(pageText.pageSections[2].sectionSubtitle ?? "");
- console.log("response.data: ", response.data);
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- }, []);
-
- /* Handle Fields upon edit */
- const handleEdit = (event: React.ChangeEvent) => {
- setIsEdited(true);
- if (event.target.id === "Page Header: Subtitle") {
- setPhSubtitle(event.target.value);
- } else if (event.target.id === "Section 1 - Upcoming Events: Subtitle") {
- setS1Subtitle(event.target.value);
- } else if (event.target.id === "Section 2 - Donate: Subtitle") {
- setS2Subtitle(event.target.value);
- }
- };
-
- const handleSave = () => {
- // Implement save logic
- if (isEdited) {
- console.log("Save changes");
- updatePage({
- //Pass edited text to MongoDB
- page: "Get Involved",
- pageSections: [
- {
- subtitle: phSubtitle,
- },
- {
- sectionSubtitle: s1Subtitle,
- },
- {
- sectionSubtitle: s2Subtitle,
- },
- ],
- })
- .then((response) => {
- if (response.success) {
- setShowAlert(true);
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- setIsEdited(false);
- }
- setWarningOpen(false);
- };
-
- const handleCancel = () => {
- // Show cancel warning
- if (isEdited) {
- setWarningOpen(true);
- }
- };
-
- const confirmCancel = () => {
- // Implement cancel logic
- setWarningOpen(false);
- console.log("Cancel changes");
- getPageText("Get Involved")
- .then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Subtitle(pageText.pageSections[1].sectionSubtitle ?? "");
- setS2Subtitle(pageText.pageSections[2].sectionSubtitle ?? "");
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- setIsEdited(false);
- };
-
- const handleCloseAlert = () => {
- setShowAlert(false);
- };
-
+export default function InvolvedEditorPage() {
return (
-
-
- {warningOpen &&
}
-
- {warningOpen && (
- {
- setWarningOpen(false);
- }}
- />
- )}
-
-
-
+
+
+
+
);
}
diff --git a/frontend/src/app/admin/page-editor/mission/MissionEditor.tsx b/frontend/src/app/admin/page-editor/mission/MissionEditor.tsx
new file mode 100644
index 00000000..72bf9036
--- /dev/null
+++ b/frontend/src/app/admin/page-editor/mission/MissionEditor.tsx
@@ -0,0 +1,110 @@
+import React, { useEffect, useState } from "react";
+
+import { getPageData, updatePageData } from "../../../../api/pageeditor";
+import Toast from "../../../../components/admin/Toast";
+import CancelButton from "../../../../components/admin/pageeditor/CancelButton";
+import { CollapsibleFields } from "../../../../components/admin/pageeditor/CollapsibleFields";
+import { usePage, usePageDispatch } from "../../../../components/admin/pageeditor/PageProvider";
+import SaveButton from "../../../../components/admin/pageeditor/SaveButton";
+
+import styles from "./page.module.css";
+
+export default function MissionEditor() {
+ const page = usePage();
+ const dispatch = usePageDispatch();
+ const [open, setOpen] = useState(false);
+
+ // Send page data from MongoDB to provider state
+ const updatePageStateFromDB = () => {
+ getPageData(page.name)
+ .then((response) => {
+ if (response.success) {
+ dispatch({
+ type: "edit_page",
+ setIsEdited: false,
+ page: {
+ ...page,
+ fields: response.data.fields,
+ },
+ });
+ } else {
+ alert("Could not connect to database. Any changes you make will not take effect!!!");
+ }
+ })
+ .catch((error) => {
+ alert(error);
+ });
+ };
+
+ // When the page loads update state to reflect page data from MongoDB
+ useEffect(() => {
+ updatePageStateFromDB();
+ }, []);
+
+ // Implement save logic
+ const handleSave = () => {
+ if (page.isEdited) {
+ // set isEdited to false or else when we load from mongo it will have wrong state
+ updatePageData(page.name, { ...page, isEdited: false })
+ .then(() => {
+ setOpen(true);
+ })
+ .catch((error) => {
+ alert(error);
+ });
+ dispatch({
+ type: "set_isEdited",
+ setIsEdited: false,
+ });
+ }
+ };
+
+ // Implement cancel logic
+ const handleCancel = () => {
+ if (page.isEdited) {
+ updatePageStateFromDB();
+ dispatch({
+ type: "set_isEdited",
+ setIsEdited: false,
+ });
+ }
+ };
+
+ return (
+
+
{
+ setOpen(false);
+ }}
+ />
+
+
+ );
+}
diff --git a/frontend/src/app/admin/page-editor/mission/page.tsx b/frontend/src/app/admin/page-editor/mission/page.tsx
index c8fb8885..1df73489 100644
--- a/frontend/src/app/admin/page-editor/mission/page.tsx
+++ b/frontend/src/app/admin/page-editor/mission/page.tsx
@@ -1,158 +1,13 @@
"use client";
-import React, { useEffect, useState } from "react";
-import { getPageText, updatePage } from "../../../../api/pageeditor";
+import PageToggle from "../../../../components/PageToggle";
+import { PageProvider } from "../../../../components/admin/pageeditor/PageProvider";
+import defaultPage from "../../../../components/admin/pageeditor/defaultPages/missionPageDefault.json";
+import MissionEditor from "./MissionEditor";
import styles from "./page.module.css";
-import Button from "@/components/Button";
-import CancelButton from "@/components/CancelButton";
-import Collapsable from "@/components/Collapsable";
-import PageToggle from "@/components/PageToggle";
-
-// import PageEditorCard from "@/components/PageEditorCard";
-
-export default function MissionEditor() {
- const [isEdited, setIsEdited] = useState(false);
- const [valueSubtitle, setvalueSubtitle] = useState("");
- const [phSubtitle, setPhSubtitle] = useState("");
- const [Value1, setValue1] = useState("");
- const [Value1_Description, setValue1_Description] = useState("");
- const [Value2, setValue2] = useState("");
- const [Value2_Description, setValue2_Description] = useState("");
- const [Value3, setValue3] = useState("");
- const [Value3_Description, setValue3_Description] = useState("");
- const [s1Text, setS1Text] = useState("");
- const [s1Subtitle, setS1Subtitle] = useState("");
-
- /* Get page data from MongoDB */
- let pageText;
- useEffect(() => {
- getPageText("Our Mission")
- .then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setvalueSubtitle(pageText.pageSections[1].subtitle ?? "");
- setValue1(pageText.pageSections[2].sectionTitle ?? "");
- setValue1_Description(pageText.pageSections[2].sectionSubtitle ?? "");
- setValue2(pageText.pageSections[3].sectionTitle ?? "");
- setValue2_Description(pageText.pageSections[3].sectionSubtitle ?? "");
- setValue3(pageText.pageSections[4].sectionTitle ?? "");
- setValue3_Description(pageText.pageSections[4].sectionSubtitle ?? "");
- setS1Subtitle(pageText.pageSections[5].sectionTitle ?? "");
- setS1Text(pageText.pageSections[5].sectionSubtitle ?? "");
- console.log("response.data: ", response.data);
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- }, []);
-
- /* Handle Fields upon edit */
- const handleEdit = (event: React.ChangeEvent) => {
- setIsEdited(true);
- if (event.target.id === "Page Subtitle: Subtitle") {
- setPhSubtitle(event.target.value);
- } else if (event.target.id === "Section 1: Section Title") {
- setvalueSubtitle(event.target.value);
- } else if (event.target.id === "Section 1: Value #1") {
- setValue1(event.target.value);
- } else if (event.target.id === "Section 1: Value #1 Description") {
- setValue1_Description(event.target.value);
- } else if (event.target.id === "Section 1: Value #2") {
- setValue2(event.target.value);
- } else if (event.target.id === "Section 1: Value #2 Description") {
- setValue2_Description(event.target.value);
- } else if (event.target.id === "Section 1: Value #3") {
- setValue3(event.target.value);
- } else if (event.target.id === "Section 1: Value #3 Description") {
- setValue3_Description(event.target.value);
- } else if (event.target.id === "Section 2: Section Title") {
- setS1Subtitle(event.target.value);
- } else if (event.target.id === "Section 2: Body Text") {
- setS1Text(event.target.value);
- }
- };
-
- const handleSave = () => {
- // Implement save logic
- if (isEdited) {
- console.log("Save changes");
- updatePage({
- //Pass edited text to MongoDB
- page: "Our Mission",
- pageSections: [
- {
- subtitle: phSubtitle,
- },
- {
- subtitle: valueSubtitle,
- },
- {
- sectionTitle: Value1,
- sectionSubtitle: Value1_Description,
- },
- {
- sectionTitle: Value2,
- sectionSubtitle: Value2_Description,
- },
- {
- sectionTitle: Value3,
- sectionSubtitle: Value3_Description,
- },
- {
- sectionTitle: s1Subtitle,
- sectionSubtitle: s1Text,
- },
- ],
- })
- .then((response) => {
- if (response.success) {
- alert("Success!");
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- setIsEdited(false);
- }
- };
-
- const handleCancel = () => {
- // Implement cancel logic
- if (isEdited) {
- console.log("Cancel changes");
- getPageText("Our Mission")
- .then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setvalueSubtitle(pageText.pageSections[1].subtitle ?? "");
- setValue1(pageText.pageSections[2].sectionTitle ?? "");
- setValue1_Description(pageText.pageSections[2].sectionSubtitle ?? "");
- setValue2(pageText.pageSections[3].sectionTitle ?? "");
- setValue2_Description(pageText.pageSections[3].sectionSubtitle ?? "");
- setValue3(pageText.pageSections[4].sectionTitle ?? "");
- setValue3_Description(pageText.pageSections[4].sectionSubtitle ?? "");
- setS1Subtitle(pageText.pageSections[5].sectionTitle ?? "");
- setS1Text(pageText.pageSections[5].sectionSubtitle ?? "");
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- setIsEdited(false);
- }
- };
-
+export default function MissionEditorPage() {
return (
-
+
+
+
+
);
}
diff --git a/frontend/src/app/admin/page-editor/newsletter/NewsletterEditor.tsx b/frontend/src/app/admin/page-editor/newsletter/NewsletterEditor.tsx
new file mode 100644
index 00000000..9c935493
--- /dev/null
+++ b/frontend/src/app/admin/page-editor/newsletter/NewsletterEditor.tsx
@@ -0,0 +1,93 @@
+import React, { useEffect, useState } from "react";
+
+import { getPageData, updatePageData } from "../../../../api/pageeditor";
+import Toast from "../../../../components/admin/Toast";
+import CancelButton from "../../../../components/admin/pageeditor/CancelButton";
+import { CollapsibleFields } from "../../../../components/admin/pageeditor/CollapsibleFields";
+import { usePage, usePageDispatch } from "../../../../components/admin/pageeditor/PageProvider";
+import SaveButton from "../../../../components/admin/pageeditor/SaveButton";
+
+import styles from "./page.module.css";
+
+export default function NewsletterEditor() {
+ const page = usePage();
+ const dispatch = usePageDispatch();
+ const [open, setOpen] = useState(false);
+
+ // Send page data from MongoDB to provider state
+ const updatePageStateFromDB = () => {
+ getPageData(page.name)
+ .then((response) => {
+ if (response.success) {
+ dispatch({
+ type: "edit_page",
+ setIsEdited: false,
+ page: {
+ ...page,
+ fields: response.data.fields,
+ },
+ });
+ } else {
+ alert("Could not connect to database. Any changes you make will not take effect!!!");
+ }
+ })
+ .catch((error) => {
+ alert(error);
+ });
+ };
+
+ // When the page loads update state to reflect page data from MongoDB
+ useEffect(() => {
+ updatePageStateFromDB();
+ }, []);
+
+ // Implement save logic
+ const handleSave = () => {
+ if (page.isEdited) {
+ // set isEdited to false or else when we load from mongo it will have wrong state
+ updatePageData(page.name, { ...page, isEdited: false })
+ .then(() => {
+ setOpen(true);
+ })
+ .catch((error) => {
+ alert(error);
+ });
+ dispatch({
+ type: "set_isEdited",
+ setIsEdited: false,
+ });
+ }
+ };
+
+ // Implement cancel logic
+ const handleCancel = () => {
+ if (page.isEdited) {
+ updatePageStateFromDB();
+ dispatch({
+ type: "set_isEdited",
+ setIsEdited: false,
+ });
+ }
+ };
+
+ return (
+
+
{
+ setOpen(false);
+ }}
+ />
+
+
+ );
+}
diff --git a/frontend/src/app/admin/page-editor/newsletter/page.tsx b/frontend/src/app/admin/page-editor/newsletter/page.tsx
index 4726c501..a3886820 100644
--- a/frontend/src/app/admin/page-editor/newsletter/page.tsx
+++ b/frontend/src/app/admin/page-editor/newsletter/page.tsx
@@ -1,107 +1,13 @@
"use client";
-import React, { useEffect, useState } from "react";
-import { getPageText, updatePage } from "../../../../api/pageeditor";
+import PageToggle from "../../../../components/PageToggle";
+import { PageProvider } from "../../../../components/admin/pageeditor/PageProvider";
+import defaultPage from "../../../../components/admin/pageeditor/defaultPages/newsletterPageDefault.json";
+import NewsletterEditor from "./NewsletterEditor";
import styles from "./page.module.css";
-import Button from "@/components/Button";
-import CancelButton from "@/components/CancelButton";
-import Collapsable from "@/components/Collapsable";
-import PageToggle from "@/components/PageToggle";
-
-// import PageEditorCard from "@/components/PageEditorCard";
-
-export default function NewsletterEditor() {
- const [isEdited, setIsEdited] = useState(false);
- const [phSubtitle, setPhSubtitle] = useState("");
- const [s1Subtitle, setS1Subtitle] = useState("");
- const [s1Text, setS1Text] = useState("");
-
- /* Get page data from MongoDB */
- let pageText;
- useEffect(() => {
- getPageText("Newsletter")
- .then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Subtitle(pageText.pageSections[1].sectionTitle ?? "");
- setS1Text(pageText.pageSections[1].sectionSubtitle ?? "");
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- }, []);
-
- /* Handle Fields upon edit */
- const handleEdit = (event: React.ChangeEvent) => {
- setIsEdited(true);
- if (event.target.id === "Page Header: Subtitle") {
- setPhSubtitle(event.target.value);
- } else if (event.target.id === "Section 1: Section Title") {
- setS1Subtitle(event.target.value);
- } else if (event.target.id === "Section 1: Section Subtitle") {
- setS1Text(event.target.value);
- }
- };
-
- const handleSave = () => {
- // Implement save logic
- if (isEdited) {
- console.log("Save changes");
- updatePage({
- //Pass edited text to MongoDB
- page: "Newsletter",
- pageSections: [
- {
- subtitle: phSubtitle,
- },
- {
- sectionTitle: s1Subtitle,
- sectionSubtitle: s1Text,
- },
- ],
- })
- .then((response) => {
- if (response.success) {
- alert("Success!");
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- setIsEdited(false);
- }
- };
-
- const handleCancel = () => {
- // Implement cancel logic
- if (isEdited) {
- console.log("Cancel changes");
- getPageText("Newsletter")
- .then((response) => {
- if (response.success) {
- pageText = response.data;
- setPhSubtitle(pageText.pageSections[0].subtitle ?? "");
- setS1Subtitle(pageText.pageSections[1].sectionTitle ?? "");
- setS1Text(pageText.pageSections[1].sectionSubtitle ?? "");
- } else {
- alert(response.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- setIsEdited(false);
- }
- };
-
+export default function NewsletterEditorPage() {
return (
-
+
+
+
);
}
diff --git a/frontend/src/app/admin/page-editor/page.module.css b/frontend/src/app/admin/page-editor/page.module.css
index 5bb35341..528c752a 100644
--- a/frontend/src/app/admin/page-editor/page.module.css
+++ b/frontend/src/app/admin/page-editor/page.module.css
@@ -1,6 +1,6 @@
.page {
display: flex;
- flex-direction: column;
+ flex-direction: column;
justify-content: center;
padding-left: 30px;
padding-top: 50px;
diff --git a/frontend/src/app/admin/page-editor/page.tsx b/frontend/src/app/admin/page-editor/page.tsx
index cc020733..29336aeb 100644
--- a/frontend/src/app/admin/page-editor/page.tsx
+++ b/frontend/src/app/admin/page-editor/page.tsx
@@ -1,7 +1,7 @@
// Admin Page Editor landing page
import styles from "./page.module.css";
-import ImageDisplay from "@/components/ImageDisplay";
+import ImageDisplay from "@/components/admin/imageStorage/GalleryDisplay";
import PageEditorCard from "@/components/PageEditorCard";
export default function PageEditorDashboard() {
diff --git a/frontend/src/app/admin/page-editor/team/page.tsx b/frontend/src/app/admin/page-editor/team/page.tsx
index 289d8564..3792b77c 100644
--- a/frontend/src/app/admin/page-editor/team/page.tsx
+++ b/frontend/src/app/admin/page-editor/team/page.tsx
@@ -4,8 +4,8 @@ import React, { useState } from "react";
import styles from "./page.module.css";
import Button from "@/components/Button";
-import CancelButton from "@/components/CancelButton";
-import Collapsable, { UploadImageTypes } from "@/components/Collapsable";
+import CancelButton from "@/components/admin/pageeditor/CancelButton";
+// import Collapsable, { UploadImageTypes } from "@/components/Collapsable";
import PageToggle from "@/components/PageToggle";
// import PageEditorCard from "@/components/PageEditorCard";
@@ -50,9 +50,9 @@ export default function TeamEditor() {
From ff2febf957c69d4d791c2a7c6ddbc52d3749cb77 Mon Sep 17 00:00:00 2001
From: Jack Hansen <47556286+jackavh@users.noreply.github.com>
Date: Wed, 29 May 2024 14:55:19 -0700
Subject: [PATCH 08/14] edit background header component to use firebase images
---
frontend/src/components/BackgroundHeader.tsx | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/frontend/src/components/BackgroundHeader.tsx b/frontend/src/components/BackgroundHeader.tsx
index a6214dfa..b1f6175e 100644
--- a/frontend/src/components/BackgroundHeader.tsx
+++ b/frontend/src/components/BackgroundHeader.tsx
@@ -21,13 +21,6 @@ const BackgroundHeader = ({
interval = 5000,
button = null,
}: BackgroundHeaderProps) => {
- backgroundImageURIs = [
- "back1.png",
- "back2.jpeg",
- "back3.png",
- // Add more URIs as needed
- ];
-
const [activeIndex, setActiveIndex] = useState(0);
const nextSlide = () => {
@@ -43,6 +36,7 @@ const BackgroundHeader = ({
};
}, [backgroundImageURIs.length, interval]);
+ console.log(backgroundImageURIs[activeIndex]);
return (
{backgroundImageURIs.map((uri, index) => (
From f6b80475b44b9218b9cdb13386c6fca9bad5ed6b Mon Sep 17 00:00:00 2001
From: Jack Hansen <47556286+jackavh@users.noreply.github.com>
Date: Thu, 30 May 2024 18:32:41 -0700
Subject: [PATCH 09/14] lots of lint fixes
---
frontend/next.config.js | 2 +-
.../newsletter/[newsletterID]/page.tsx | 40 +++---
.../src/app/(web app)/newsletter/page.tsx | 14 +--
frontend/src/app/(web app)/page.tsx | 12 +-
frontend/src/app/(web app)/team/page.tsx | 68 +---------
frontend/src/app/admin/mailing-list/page.tsx | 116 +++++++++---------
.../src/app/admin/newsletter-creator/page.tsx | 6 +-
frontend/src/app/admin/page-editor/page.tsx | 5 +-
.../src/app/admin/page-editor/team/page.tsx | 68 +---------
frontend/src/app/admin/test-image/page.tsx | 11 --
frontend/src/components/Collapsable.tsx | 46 +++----
frontend/src/components/ContactInfoCard.tsx | 15 ++-
frontend/src/components/HeaderBar.tsx | 20 ++-
frontend/src/components/ImageDisplay.tsx | 40 ------
frontend/src/components/ImageDropzone.tsx | 42 -------
frontend/src/components/ValueCard.tsx | 10 +-
.../defaultPages/aboutPageDefault.json | 2 +-
.../defaultPages/contactPageDefault.json | 2 +-
.../defaultPages/eventsPageDefault.json | 2 +-
.../defaultPages/homePageDefault.json | 4 +-
.../defaultPages/impactPageDefault.json | 2 +-
.../defaultPages/involvedPageDefault.json | 2 +-
.../defaultPages/missionPageDefault.json | 2 +-
.../defaultPages/newsletterPageDefault.json | 2 +-
.../pageeditor/inputBoxes/TextFieldBox.tsx | 3 +-
25 files changed, 168 insertions(+), 368 deletions(-)
delete mode 100644 frontend/src/app/admin/test-image/page.tsx
delete mode 100644 frontend/src/components/ImageDisplay.tsx
delete mode 100644 frontend/src/components/ImageDropzone.tsx
diff --git a/frontend/next.config.js b/frontend/next.config.js
index adddda7d..ba9bb05e 100644
--- a/frontend/next.config.js
+++ b/frontend/next.config.js
@@ -9,7 +9,7 @@ const nextConfig = {
"images.unsplash.com",
"plus.unsplash.com",
"firebasestorage.googleapis.com",
- "tse.ucsd.edu"
+ "tse.ucsd.edu",
],
},
};
diff --git a/frontend/src/app/(web app)/newsletter/[newsletterID]/page.tsx b/frontend/src/app/(web app)/newsletter/[newsletterID]/page.tsx
index c308a9dd..4eeacdf0 100644
--- a/frontend/src/app/(web app)/newsletter/[newsletterID]/page.tsx
+++ b/frontend/src/app/(web app)/newsletter/[newsletterID]/page.tsx
@@ -1,15 +1,16 @@
"use client";
+
+import Image from "next/image";
import React, { useEffect, useState } from "react";
import { Newsletter, getNewsletter } from "../../../../api/newsletter";
+import { GalleryData, getPageData } from "../../../../api/pageeditor";
+import BackgroundHeader from "../../../../components/BackgroundHeader";
+import Button from "../../../../components/Button";
import NewsletterPopup from "../../../../components/NewsletterPopup";
import styles from "./page.module.css";
-import { BackgroundImage, BackgroundImagePages, getBackgroundImages } from "@/api/images";
-import BackgroundHeader from "@/components/BackgroundHeader";
-import Button from "@/components/Button";
-
type Props = {
params: { newsletterID: string };
};
@@ -18,18 +19,17 @@ export default function NewsletterDisplay({ params }: Props) {
const [popupOpen, setPopup] = useState(false);
const [newsletter, setNewsletter] = useState(null);
- const [images, setImages] = useState([]);
+ const [images, setImages] = useState([]);
useEffect(() => {
- getBackgroundImages(BackgroundImagePages.TEAM)
- .then((result) => {
- if (result.success) {
- setImages(result.data);
- }
+ getPageData("newsletter")
+ .then((response) => {
+ if (response.success) {
+ const carouselImages = (response.data.fields[1].data as GalleryData).images;
+ setImages(carouselImages);
+ } else throw new Error(response.error);
})
- .catch((error) => {
- alert(error);
- });
+ .catch(console.error);
}, []);
const handleSubscribeClick = () => {
@@ -53,7 +53,7 @@ export default function NewsletterDisplay({ params }: Props) {
return (
image.imageURI)}
+ backgroundImageURIs={images}
header=""
title="The 4FLOT Quarterly"
description="4FLOT is committed in preventing and ending homelessness, hunger and disparity in underprivileged communities. "
@@ -69,7 +69,7 @@ export default function NewsletterDisplay({ params }: Props) {
-
Here’s Our Story
@@ -90,10 +92,12 @@ export default function NewsletterDisplay({ params }: Props) {
Share This Post
-
-
diff --git a/frontend/src/app/(web app)/newsletter/page.tsx b/frontend/src/app/(web app)/newsletter/page.tsx
index aa8914eb..0427ef20 100644
--- a/frontend/src/app/(web app)/newsletter/page.tsx
+++ b/frontend/src/app/(web app)/newsletter/page.tsx
@@ -24,13 +24,6 @@ export default function NewsletterPage() {
const [pageMap, setPageMap] = useState
>();
const [loading, setLoading] = useState(false);
- useEffect(() => {
- setLoading(true);
- loadNewspapers();
- loadPageMap();
- setLoading(false);
- }, []);
-
function loadPageMap() {
getPageData("newsletter")
.then((response) => {
@@ -82,6 +75,13 @@ export default function NewsletterPage() {
});
}
+ useEffect(() => {
+ setLoading(true);
+ loadNewspapers();
+ loadPageMap();
+ setLoading(false);
+ }, []);
+
const handleSubscribeClick = () => {
setPopup(true);
};
diff --git a/frontend/src/app/(web app)/page.tsx b/frontend/src/app/(web app)/page.tsx
index 91bf8a4e..058f5082 100644
--- a/frontend/src/app/(web app)/page.tsx
+++ b/frontend/src/app/(web app)/page.tsx
@@ -1,5 +1,6 @@
"use client";
+import Image from "next/image";
import React, { useEffect, useState } from "react";
import { getPageData } from "../../api/pageeditor";
@@ -71,10 +72,17 @@ export default function Home() {
description={pageMap.get("Sponsors Body Text") as string}
/>
-
+
{(pageMap.get("Sponsor Image Gallery") as string[]).map((url) => (
-
+
))}
diff --git a/frontend/src/app/(web app)/team/page.tsx b/frontend/src/app/(web app)/team/page.tsx
index 6d6db08e..7c6dd45c 100644
--- a/frontend/src/app/(web app)/team/page.tsx
+++ b/frontend/src/app/(web app)/team/page.tsx
@@ -1,69 +1,3 @@
-"use client";
-
-import React, { useEffect, useState } from "react";
-
-import styles from "./page.module.css";
-
-import { BackgroundImage, BackgroundImagePages, getBackgroundImages } from "@/api/images";
-import { Member, getAllMembers } from "@/api/member";
-import BackgroundHeader from "@/components/BackgroundHeader";
-import MemberInfo from "@/components/MemberInfo";
-
export default function Team() {
- const [members, setMembers] = useState
([]);
- const [images, setImages] = useState([]);
-
- useEffect(() => {
- getBackgroundImages(BackgroundImagePages.TEAM)
- .then((result) => {
- if (result.success) {
- setImages(result.data);
- }
- })
- .catch((error) => {
- alert(error);
- });
- }, []);
-
- useEffect(() => {
- getAllMembers()
- .then((result) => {
- if (result.success) {
- console.log(result.data);
- setMembers(result.data);
- } else {
- alert(result.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- }, []);
- return (
-
-
image.imageURI)}
- header="OUR TEAM"
- title="Meet Our Team"
- description="Lorem ipsum dolor sit amet consectetur. Et vestibulum enim nunc ultrices. Donec blandit
- sollicitudin vitae integer mauris sed. Mattis duis id viverra suscipit morbi."
- />
-
-
Our Team
- {/*
Hello.
*/}
-
- Our dedicated team @ 4 Future Leaders of Tomorrow is a non-profit charitable organization
- committed in preventing and ending homelessness, hunger and disparity in underprivileged
- communities. Everyone deserves a chance for a better future!. We are reaching out by
- providing resources in needed communities - whether it be a delicious meal, warm clothing,
- educational supplies, referrals, toys or even bus passes
-
-
-
- {members.map((member) => (
-
- ))}
-
-
- );
+ return This will be the team page. ;
}
diff --git a/frontend/src/app/admin/mailing-list/page.tsx b/frontend/src/app/admin/mailing-list/page.tsx
index 2d916b3a..aa5bde0a 100644
--- a/frontend/src/app/admin/mailing-list/page.tsx
+++ b/frontend/src/app/admin/mailing-list/page.tsx
@@ -25,62 +25,6 @@ import RowCopyBtn from "@/components/RowCopyBtn";
import RowDeleteBtn from "@/components/RowDeleteBtn";
export default function MailingList() {
- const columns: GridColDef<(typeof rows)[number]>[] = [
- {
- field: "lastName",
- headerName: "Last name",
- width: 280,
- editable: false,
- 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: false,
- 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: false,
- 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: false,
- resizable: false,
- flex: 1,
- headerClassName: `${styles.Headings} ${styles.headingBackground}`,
- cellClassName: styles.cellEntry,
- disableColumnMenu: true,
- renderHeader: () => (
-
- ),
- },
- ];
-
const [rows, setRow] = useState([]);
const [rowsCurrent, setRowsCurrent] = React.useState(rows);
const [alertType, setAlertType] = useState("");
@@ -286,6 +230,62 @@ export default function MailingList() {
}
};
+ const columns: GridColDef<(typeof rows)[number]>[] = [
+ {
+ field: "lastName",
+ headerName: "Last name",
+ width: 280,
+ editable: false,
+ 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: false,
+ 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: false,
+ 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: false,
+ resizable: false,
+ flex: 1,
+ headerClassName: `${styles.Headings} ${styles.headingBackground}`,
+ cellClassName: styles.cellEntry,
+ disableColumnMenu: true,
+ renderHeader: () => (
+
+ ),
+ },
+ ];
+
return (
@@ -350,9 +350,11 @@ export default function MailingList() {
lineHeight: "24px",
}} // Make room for the image
/>
-
-
+
Add Newsletter
diff --git a/frontend/src/app/admin/page-editor/page.tsx b/frontend/src/app/admin/page-editor/page.tsx
index 29336aeb..1ec436bf 100644
--- a/frontend/src/app/admin/page-editor/page.tsx
+++ b/frontend/src/app/admin/page-editor/page.tsx
@@ -1,8 +1,7 @@
// Admin Page Editor landing page
-import styles from "./page.module.css";
+import PageEditorCard from "../../../components/PageEditorCard";
-import ImageDisplay from "@/components/admin/imageStorage/GalleryDisplay";
-import PageEditorCard from "@/components/PageEditorCard";
+import styles from "./page.module.css";
export default function PageEditorDashboard() {
return (
diff --git a/frontend/src/app/admin/page-editor/team/page.tsx b/frontend/src/app/admin/page-editor/team/page.tsx
index 3792b77c..98ec0e0f 100644
--- a/frontend/src/app/admin/page-editor/team/page.tsx
+++ b/frontend/src/app/admin/page-editor/team/page.tsx
@@ -1,69 +1,3 @@
-"use client";
-import React, { useState } from "react";
-
-import styles from "./page.module.css";
-
-import Button from "@/components/Button";
-import CancelButton from "@/components/admin/pageeditor/CancelButton";
-// import Collapsable, { UploadImageTypes } from "@/components/Collapsable";
-import PageToggle from "@/components/PageToggle";
-
-// import PageEditorCard from "@/components/PageEditorCard";
-
export default function TeamEditor() {
- const [isEdited, setIsEdited] = useState(false);
- const handleEdit = () => {
- setIsEdited(true);
- };
-
- const handleSave = () => {
- // Implement save logic
- console.log("Save changes");
- setIsEdited(false);
- };
-
- const handleCancel = () => {
- // Implement cancel logic
- console.log("Cancel changes");
- setIsEdited(false);
- };
-
- return (
-
-
-
-
- );
+ return
This will be the /team page editor. ;
}
diff --git a/frontend/src/app/admin/test-image/page.tsx b/frontend/src/app/admin/test-image/page.tsx
deleted file mode 100644
index 222ec1d7..00000000
--- a/frontend/src/app/admin/test-image/page.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react';
-
-import ImageDisplay from '@/components/ImageDisplay';
-
-export default function Page() {
- return (
-
{/* Tailwind classes for centering and padding */}
-
-
- );
-}
\ No newline at end of file
diff --git a/frontend/src/components/Collapsable.tsx b/frontend/src/components/Collapsable.tsx
index 8b0029a0..885e1d45 100644
--- a/frontend/src/components/Collapsable.tsx
+++ b/frontend/src/components/Collapsable.tsx
@@ -1,31 +1,21 @@
"use client";
import Image from "next/image";
-import React, { useEffect, useState } from "react";
+import React, { useState } from "react";
import styles from "./Collapsable.module.css";
-import ImageDisplay from "./ImageDisplay";
-
-
-export enum UploadImageTypes {
- SPONSORS = "Sponsors",
- OUR_MISSION = "Our_Mission",
- OUR_TEAM = "Our_Team",
- CONTACT_US = "Contact_Us"
-}
-
+import { usePage } from "./admin/pageeditor/PageProvider";
+import { GalleryBox } from "./admin/pageeditor/inputBoxes/GalleryBox";
type CollapsableProps = {
title: string;
subsection: string[];
textbox: string[];
- imageUploadBox: UploadImageTypes | undefined;
- images: string[] | undefined;
- setImages : (images: string[]) => void;
onChange: (event: React.ChangeEvent
) => void;
};
-const Collapsable = ({ title, subsection, textbox, onChange, imageUploadBox, images, setImages }: CollapsableProps) => {
+const Collapsable = ({ title, subsection, textbox, onChange }: CollapsableProps) => {
const [open, setOpen] = useState(true);
+ const page = usePage();
const toggleSection = () => {
setOpen(!open);
@@ -33,19 +23,16 @@ const Collapsable = ({ title, subsection, textbox, onChange, imageUploadBox, ima
const handleChange = (event: React.ChangeEvent) => {
// Auto increase height when typing
- if(event && event.target){
- event.target.style.height = "auto";
- event.target.style.height = 2 + event.target.scrollHeight + "px";
+ if (event && event.target) {
+ event.target.style.height = "auto";
+ event.target.style.height = 2 + event.target.scrollHeight + "px";
}
// Call onChange function
onChange(event);
};
- useEffect(() => {
- if(images){ //mark change in page when image is uploaded
- handleChange({} as React.ChangeEvent);
- }
- }, [images])
+ const field = page.fields[page.fields.findIndex((f) => f.name === title)];
+ const hasGalleryBox = field.type === "gallery";
return (
@@ -78,13 +65,12 @@ const Collapsable = ({ title, subsection, textbox, onChange, imageUploadBox, ima
);
})}
- {imageUploadBox &&
- }
+ {hasGalleryBox && (
+
+ )}
)}
diff --git a/frontend/src/components/ContactInfoCard.tsx b/frontend/src/components/ContactInfoCard.tsx
index 696fa46b..6501e1ec 100644
--- a/frontend/src/components/ContactInfoCard.tsx
+++ b/frontend/src/components/ContactInfoCard.tsx
@@ -1,3 +1,4 @@
+import Image from "next/image";
import React from "react";
import styles from "./ContactInfoCard.module.css";
@@ -11,13 +12,19 @@ type ContactInfoCardProps = {
const ContactInfoCard = ({ iconSrc, title, description }: ContactInfoCardProps) => {
return (
-
+
{title}
- {description.map((txt) => (
- // eslint-disable-next-line react/jsx-key
-
{txt}
+ {description.map((txt, idx) => (
+
{txt}
))}
diff --git a/frontend/src/components/HeaderBar.tsx b/frontend/src/components/HeaderBar.tsx
index fed5fa5b..f70c41d2 100644
--- a/frontend/src/components/HeaderBar.tsx
+++ b/frontend/src/components/HeaderBar.tsx
@@ -1,4 +1,5 @@
"use client";
+import Image from "next/image";
import Link from "next/link";
import React, { useState } from "react";
@@ -14,16 +15,29 @@ const HeaderBar = () => {
return (
- {/*
*/}
-
+
-
+
diff --git a/frontend/src/components/ImageDisplay.tsx b/frontend/src/components/ImageDisplay.tsx
deleted file mode 100644
index 0c096c61..00000000
--- a/frontend/src/components/ImageDisplay.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-"use client"
-
-import Image from "next/image";
-import { useState } from "react";
-
-import { UploadImageTypes } from "./Collapsable";
-import ImageDropzone from "./ImageDropzone";
-
-
-type ImageDisplayProps = {
- type : UploadImageTypes
- images: string[]
- setImages : (images: string[]) => void
-}
-
-
-export default function ImageDisplay({type, images, setImages} : ImageDisplayProps) {
-
- return (
-
- {images.map((image, index) => (
-
- ))}
-
-
-
-
- );
- }
\ No newline at end of file
diff --git a/frontend/src/components/ImageDropzone.tsx b/frontend/src/components/ImageDropzone.tsx
deleted file mode 100644
index c95c5737..00000000
--- a/frontend/src/components/ImageDropzone.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-"use client"
-import { getDownloadURL, ref, uploadBytes } from 'firebase/storage';
-import React, { useEffect, useState } from 'react';
-import Dropzone from 'react-dropzone';
-import {MdFileUpload} from 'react-icons/md';
-import {useStorage} from 'reactfire';
-type FileDropzoneProps = {
- setImages: (images: string[]) => void;
- type: string;
-}
-export default function FileDropzone({ setImages, type }: FileDropzoneProps) {
-
- const storage = useStorage();
- const [uploading, setUploading] = useState(false);
-
- async function uploadImageToFirebase(file: File) {
- setUploading(true);
- const storageRef = ref(storage, `${type}/${file.name}`);
- try {
- const uploadResult = await uploadBytes(storageRef, file);
- const downloadURL = await getDownloadURL(uploadResult.ref);
- setImages((images) => [...images, downloadURL]);
- } catch (error) {
- console.error("Error uploading file:", error);
- } finally {
- setUploading(false);
- }
- }
-
-
-return (
-
uploadImageToFirebase(acceptedFiles[0])}>
- {({ getRootProps, getInputProps }) => (
-
-
-
{/* Arrow icon */}
- {uploading ?
Uploading...
:
Upload an Image
}
-
- )}
-
-);
-}
\ No newline at end of file
diff --git a/frontend/src/components/ValueCard.tsx b/frontend/src/components/ValueCard.tsx
index 5cb703f3..f85f62ba 100644
--- a/frontend/src/components/ValueCard.tsx
+++ b/frontend/src/components/ValueCard.tsx
@@ -1,3 +1,4 @@
+import Image from "next/image";
import React from "react";
import styles from "./ValueCard.module.css";
@@ -12,7 +13,14 @@ const ValueCard = ({ iconSrc, title, description }: ValueCardProps) => {
return (
-
+
{title}
{description}
diff --git a/frontend/src/components/admin/pageeditor/defaultPages/aboutPageDefault.json b/frontend/src/components/admin/pageeditor/defaultPages/aboutPageDefault.json
index f07876a6..bcf788a0 100644
--- a/frontend/src/components/admin/pageeditor/defaultPages/aboutPageDefault.json
+++ b/frontend/src/components/admin/pageeditor/defaultPages/aboutPageDefault.json
@@ -84,4 +84,4 @@
}
}
]
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/admin/pageeditor/defaultPages/contactPageDefault.json b/frontend/src/components/admin/pageeditor/defaultPages/contactPageDefault.json
index de820a2c..956f0f47 100644
--- a/frontend/src/components/admin/pageeditor/defaultPages/contactPageDefault.json
+++ b/frontend/src/components/admin/pageeditor/defaultPages/contactPageDefault.json
@@ -38,4 +38,4 @@
}
}
]
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/admin/pageeditor/defaultPages/eventsPageDefault.json b/frontend/src/components/admin/pageeditor/defaultPages/eventsPageDefault.json
index 931bfa40..8bf50ed4 100644
--- a/frontend/src/components/admin/pageeditor/defaultPages/eventsPageDefault.json
+++ b/frontend/src/components/admin/pageeditor/defaultPages/eventsPageDefault.json
@@ -32,4 +32,4 @@
}
}
]
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/admin/pageeditor/defaultPages/homePageDefault.json b/frontend/src/components/admin/pageeditor/defaultPages/homePageDefault.json
index df9726b8..08f6a535 100644
--- a/frontend/src/components/admin/pageeditor/defaultPages/homePageDefault.json
+++ b/frontend/src/components/admin/pageeditor/defaultPages/homePageDefault.json
@@ -1,4 +1,4 @@
- {
+{
"name": "home",
"isEdited": false,
"fields": [
@@ -54,4 +54,4 @@
}
}
]
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/admin/pageeditor/defaultPages/impactPageDefault.json b/frontend/src/components/admin/pageeditor/defaultPages/impactPageDefault.json
index bfbc0b2a..f9c6abe4 100644
--- a/frontend/src/components/admin/pageeditor/defaultPages/impactPageDefault.json
+++ b/frontend/src/components/admin/pageeditor/defaultPages/impactPageDefault.json
@@ -32,4 +32,4 @@
}
}
]
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/admin/pageeditor/defaultPages/involvedPageDefault.json b/frontend/src/components/admin/pageeditor/defaultPages/involvedPageDefault.json
index 602f84c0..01c84f1e 100644
--- a/frontend/src/components/admin/pageeditor/defaultPages/involvedPageDefault.json
+++ b/frontend/src/components/admin/pageeditor/defaultPages/involvedPageDefault.json
@@ -32,4 +32,4 @@
}
}
]
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/admin/pageeditor/defaultPages/missionPageDefault.json b/frontend/src/components/admin/pageeditor/defaultPages/missionPageDefault.json
index c0fc6d97..02cc3d4a 100644
--- a/frontend/src/components/admin/pageeditor/defaultPages/missionPageDefault.json
+++ b/frontend/src/components/admin/pageeditor/defaultPages/missionPageDefault.json
@@ -89,4 +89,4 @@
}
}
]
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/admin/pageeditor/defaultPages/newsletterPageDefault.json b/frontend/src/components/admin/pageeditor/defaultPages/newsletterPageDefault.json
index f22a613d..e21b82c6 100644
--- a/frontend/src/components/admin/pageeditor/defaultPages/newsletterPageDefault.json
+++ b/frontend/src/components/admin/pageeditor/defaultPages/newsletterPageDefault.json
@@ -32,4 +32,4 @@
}
}
]
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/admin/pageeditor/inputBoxes/TextFieldBox.tsx b/frontend/src/components/admin/pageeditor/inputBoxes/TextFieldBox.tsx
index 66d11943..8afa2582 100644
--- a/frontend/src/components/admin/pageeditor/inputBoxes/TextFieldBox.tsx
+++ b/frontend/src/components/admin/pageeditor/inputBoxes/TextFieldBox.tsx
@@ -1,6 +1,5 @@
-import { ChangeEvent } from "react";
-
import { TextareaAutosize } from "@mui/material";
+import { ChangeEvent } from "react";
import { Field, TextData } from "../../../../api/pageeditor";
import { usePageDispatch } from "../PageProvider";
From 582ac372ad6773baa4617d52bf502ad6ea554afd Mon Sep 17 00:00:00 2001
From: Jack Hansen <47556286+jackavh@users.noreply.github.com>
Date: Wed, 5 Jun 2024 14:42:44 -0700
Subject: [PATCH 10/14] lint fixes
---
.../components/NewsletterSidebar.module.css | 12 +-
frontend/src/components/NewsletterSidebar.tsx | 121 +++++++++---------
frontend/src/components/WarningModule.tsx | 4 +-
3 files changed, 69 insertions(+), 68 deletions(-)
diff --git a/frontend/src/components/NewsletterSidebar.module.css b/frontend/src/components/NewsletterSidebar.module.css
index cd63f841..b78d3f9e 100644
--- a/frontend/src/components/NewsletterSidebar.module.css
+++ b/frontend/src/components/NewsletterSidebar.module.css
@@ -43,6 +43,7 @@
.sidebarContents {
padding: 60px;
padding-left: 35px;
+ width: 38vw;
}
.sidebarContents h1 {
@@ -82,7 +83,7 @@
.sidebar button {
display: flex;
- padding: 4px 16px;
+ padding: 0 16px;
justify-content: center;
align-items: center;
gap: 6px;
@@ -130,8 +131,9 @@
direction: row;
gap: 24px;
justify-content: flex-end;
- margin-right: 60px;
- margin-bottom: 77px;
+ width: 33vw;
+ /* margin-right: 60px; */
+ /* margin-bottom: 77px; */
}
.deleteButtonWrapper {
@@ -159,11 +161,11 @@
}
.textField {
- width: 454px;
+ width: 33vw;
}
.textArea {
- width: 454px;
+ width: 33vw;
height: 288px;
padding: 12px;
border: 1px solid #d8d8d8;
diff --git a/frontend/src/components/NewsletterSidebar.tsx b/frontend/src/components/NewsletterSidebar.tsx
index 3dbf5017..8ffeac69 100644
--- a/frontend/src/components/NewsletterSidebar.tsx
+++ b/frontend/src/components/NewsletterSidebar.tsx
@@ -4,11 +4,10 @@ import React, { useState } from "react";
import { CreateNewsletterRequest, Newsletter, deleteNewsletter } from "../api/newsletter";
+import AlertBanner from "./AlertBanner";
import styles from "./NewsletterSidebar.module.css";
-
-import AlertBanner from "@/components/AlertBanner";
-import { TextField } from "@/components/TextField";
-import { WarningModule } from "@/components/WarningModule";
+import { TextField } from "./TextField";
+import { WarningModule } from "./WarningModule";
type newsletterSidebarProps = {
newsletter: null | Newsletter;
@@ -37,7 +36,6 @@ const NewsletterSidebar = ({
const [isEditing, setIsEditing] = useState
(!newsletter);
const [isDeleting, setIsDeleting] = useState(false);
const [errors, setErrors] = useState({});
- const [warningOpen, setWarningOpen] = useState(false);
const [showAlert, setShowAlert] = useState(false);
const confirmCancel = () => {
@@ -48,23 +46,9 @@ const NewsletterSidebar = ({
setIsEditing(false);
setIsDeleting(false);
setErrors({});
- setWarningOpen(false);
setSidebarOpen(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 : "") ||
@@ -72,7 +56,7 @@ const NewsletterSidebar = ({
date !== (newsletter ? newsletter.date : "") ||
content !== (newsletter ? newsletter.content : [])
) {
- setWarningOpen(true);
+ // not
} else {
confirmCancel();
setSidebarOpen(false);
@@ -80,7 +64,6 @@ const NewsletterSidebar = ({
};
const handleSave = () => {
- setWarningOpen(false);
if (title === "" || description === "" || date === "" || content.length === 0) {
setErrors({
title: title === "",
@@ -116,10 +99,6 @@ const NewsletterSidebar = ({
};
const handleDelete = () => {
- setIsDeleting(true);
- };
-
- const confirmDelete = () => {
if (newsletter) {
deleteNewsletter(newsletter._id)
.then((result) => {
@@ -188,21 +167,26 @@ const NewsletterSidebar = ({
{content}
{/* Delete button */}
-
-
+ action={handleDelete}
+ >
+
+
{
+ setIsDeleting(true);
+ }}
+ className={styles.deleteButton}
+ >
+ Delete
+
+
+
+
);
@@ -211,20 +195,6 @@ const NewsletterSidebar = ({
if (isEditing) {
return (
- {warningOpen &&
}
- {warningOpen && (
-
{
- setWarningOpen(false);
- }}
- />
- )}
{
@@ -282,15 +252,28 @@ const NewsletterSidebar = ({
-
- {/* Cancel button */}
-
- Cancel
-
- {/* Save button */}
-
- Save
-
+
+
+ {/* Cancel button */}
+
+
+
+ {/* Save button */}
+
+ Save
+
+
);
@@ -339,11 +322,25 @@ const NewsletterSidebar = ({
Newsletter Content
{content}
{/* Delete button */}
-
+
+
+
{
+ setIsDeleting(true);
+ }}
+ className={styles.deleteButton}
+ >
+ Delete
+
+
+
);
diff --git a/frontend/src/components/WarningModule.tsx b/frontend/src/components/WarningModule.tsx
index 91975068..8208bbed 100644
--- a/frontend/src/components/WarningModule.tsx
+++ b/frontend/src/components/WarningModule.tsx
@@ -73,7 +73,9 @@ export const WarningModule = ({
-
{children}
+
+ {children}
+
);
};
From 1c42bd76fa4a9b55ec73b86561c74f6f74f60d8d Mon Sep 17 00:00:00 2001
From: Jack Hansen <47556286+jackavh@users.noreply.github.com>
Date: Sat, 8 Jun 2024 18:27:23 -0700
Subject: [PATCH 11/14] add image upload to newsletter editor
---
.../newsletter/[newsletterID]/page.tsx | 2 +-
frontend/src/components/Footer.tsx | 2 +-
frontend/src/components/NewsletterSidebar.tsx | 86 ++++++++++++++++---
frontend/src/components/WarningModule.tsx | 4 +-
.../admin/pageeditor/inputBoxes/MemberBox.tsx | 6 +-
.../pageeditor/inputBoxes/TestimonialsBox.tsx | 6 +-
.../admin/storage/SimpleImageDropzone.tsx | 14 ++-
7 files changed, 97 insertions(+), 23 deletions(-)
diff --git a/frontend/src/app/(web app)/newsletter/[newsletterID]/page.tsx b/frontend/src/app/(web app)/newsletter/[newsletterID]/page.tsx
index 4eeacdf0..7fa09f91 100644
--- a/frontend/src/app/(web app)/newsletter/[newsletterID]/page.tsx
+++ b/frontend/src/app/(web app)/newsletter/[newsletterID]/page.tsx
@@ -70,7 +70,7 @@ export default function NewsletterDisplay({ params }: Props) {
{
width={262}
height={92.95}
className={styles.logoImage}
- src="footerLogo.svg"
+ src="/footerLogo.svg"
alt="4 Future Leaders of Tomorrow Logo"
/>
diff --git a/frontend/src/components/NewsletterSidebar.tsx b/frontend/src/components/NewsletterSidebar.tsx
index 8ffeac69..115993e5 100644
--- a/frontend/src/components/NewsletterSidebar.tsx
+++ b/frontend/src/components/NewsletterSidebar.tsx
@@ -8,6 +8,8 @@ import AlertBanner from "./AlertBanner";
import styles from "./NewsletterSidebar.module.css";
import { TextField } from "./TextField";
import { WarningModule } from "./WarningModule";
+import SimpleImageDropzone from "./admin/storage/SimpleImageDropzone";
+import { deleteFile } from "@/app/admin/util/pageeditUtil";
type newsletterSidebarProps = {
newsletter: null | Newsletter;
@@ -20,6 +22,7 @@ type formErrors = {
title?: boolean;
description?: boolean;
date?: boolean;
+ image?: boolean;
content?: boolean;
};
@@ -32,6 +35,7 @@ const NewsletterSidebar = ({
const [title, setTitle] = useState(newsletter ? newsletter.title : "");
const [description, setDescription] = useState(newsletter ? newsletter.description : "");
const [date, setDate] = useState(newsletter ? newsletter.date : "");
+ const [image, setImage] = useState(newsletter ? newsletter.image : "");
const [content, setContent] = useState(newsletter ? newsletter.content : "");
const [isEditing, setIsEditing] = useState(!newsletter);
const [isDeleting, setIsDeleting] = useState(false);
@@ -42,6 +46,7 @@ const NewsletterSidebar = ({
setTitle(newsletter ? newsletter.title : "");
setDescription(newsletter ? newsletter.description : "");
setDate(newsletter ? newsletter.date : "");
+ setImage(newsletter ? newsletter.image : "");
setContent(newsletter ? newsletter.content : "");
setIsEditing(false);
setIsDeleting(false);
@@ -54,6 +59,7 @@ const NewsletterSidebar = ({
title !== (newsletter ? newsletter.title : "") ||
description !== (newsletter ? newsletter.description : "") ||
date !== (newsletter ? newsletter.date : "") ||
+ image !== (newsletter ? newsletter.image : "") ||
content !== (newsletter ? newsletter.content : [])
) {
// not
@@ -64,11 +70,12 @@ const NewsletterSidebar = ({
};
const handleSave = () => {
- if (title === "" || description === "" || date === "" || content.length === 0) {
+ if (title === "" || description === "" || date === "" || image === "" || content.length === 0) {
setErrors({
title: title === "",
description: description === "",
date: date === "",
+ image: image === "",
content: content.length === 0,
});
} else {
@@ -76,18 +83,18 @@ const NewsletterSidebar = ({
if (newsletter) {
updateNewsletter({
_id: newsletter._id,
- image: newsletter.image,
title,
description,
date,
+ image,
content,
});
} else {
createNewsletter({
- image: "/newsletter2.png",
title,
description,
date,
+ image,
content,
});
}
@@ -98,8 +105,34 @@ const NewsletterSidebar = ({
}
};
+ // handle changing url on newsletter to "" if user deletes image
+ const onImageDelete = () => {
+ setImage("");
+ // immediately update newsletter, can't undo image delete
+ if (newsletter) {
+ updateNewsletter({
+ ...newsletter,
+ image: "",
+ });
+ }
+ };
+
+ // handle updating image on image dropzone upload
+ const onImageUpload = (url: string) => {
+ // can't undo image upload, save immediately
+ if (newsletter) {
+ updateNewsletter({
+ ...newsletter,
+ image: url,
+ });
+ }
+ };
+
const handleDelete = () => {
if (newsletter) {
+ // delete image from firebase
+ deleteFile(image).catch(console.error);
+ // delete newsletter
deleteNewsletter(newsletter._id)
.then((result) => {
if (result.success) {
@@ -162,7 +195,14 @@ const NewsletterSidebar = ({
Date & Time
{date}
Newsletter Cover
- Placeholder - to be replaced with image
+
+ {/* Placeholder - to be replaced with image
*/}
Newsletter Content
{content}
{/* Delete button */}
@@ -195,15 +235,21 @@ const NewsletterSidebar = ({
if (isEditing) {
return (
-
{
- handleCloseSidebar();
+
{
+ setSidebarOpen(false);
}}
>
-
- Close Window
-
+
+
Newsletter Details
@@ -238,7 +284,14 @@ const NewsletterSidebar = ({
error={errors.date}
/>
Newsletter Cover
-
Placeholder - to be replaced with image
+
+ {/*
Placeholder - to be replaced with image
*/}
Newsletter Content
-
+
{children}
-
+
);
};
diff --git a/frontend/src/components/admin/pageeditor/inputBoxes/MemberBox.tsx b/frontend/src/components/admin/pageeditor/inputBoxes/MemberBox.tsx
index cebd1518..b0c82faa 100644
--- a/frontend/src/components/admin/pageeditor/inputBoxes/MemberBox.tsx
+++ b/frontend/src/components/admin/pageeditor/inputBoxes/MemberBox.tsx
@@ -105,7 +105,11 @@ export default function MemberBox({
diff --git a/frontend/src/components/admin/pageeditor/inputBoxes/TestimonialsBox.tsx b/frontend/src/components/admin/pageeditor/inputBoxes/TestimonialsBox.tsx
index b5a359fe..380acc5a 100644
--- a/frontend/src/components/admin/pageeditor/inputBoxes/TestimonialsBox.tsx
+++ b/frontend/src/components/admin/pageeditor/inputBoxes/TestimonialsBox.tsx
@@ -115,7 +115,11 @@ export default function TestimonialsBox({
diff --git a/frontend/src/components/admin/storage/SimpleImageDropzone.tsx b/frontend/src/components/admin/storage/SimpleImageDropzone.tsx
index 5388effe..779b7ca3 100644
--- a/frontend/src/components/admin/storage/SimpleImageDropzone.tsx
+++ b/frontend/src/components/admin/storage/SimpleImageDropzone.tsx
@@ -4,25 +4,30 @@ import { useUploadFile } from "react-firebase-hooks/storage";
import { createUniqueFilename, deleteFile } from "../../../app/admin/util/pageeditUtil";
import { WarningModule } from "../../WarningModule";
-import { usePage } from "../pageeditor/PageProvider";
import { DeleteIcon, PhotoIcon, UploadIcon } from "./imageIcons";
type ImageDropProps = {
+ folder: string;
url: string;
setUrl: (url: string) => void;
onDelete?: () => void;
+ onUpload?: (url: string) => void;
};
-export default function SimpleImageDropzone({ url, setUrl, onDelete }: ImageDropProps) {
+export default function SimpleImageDropzone({
+ folder,
+ url,
+ setUrl,
+ onDelete,
+ onUpload,
+}: ImageDropProps) {
const storage = getStorage();
const [uploadFile, uploading, snapshot, error] = useUploadFile();
const accept = {
"image/*": [".jpeg", ".jpg", ".png"],
};
- const page = usePage();
- const folder = page.name.toLowerCase().replace(/\s/g, "");
const hasImage = url !== "";
async function upload(file: File) {
@@ -43,6 +48,7 @@ export default function SimpleImageDropzone({ url, setUrl, onDelete }: ImageDrop
upload(image)
.then((u) => {
setUrl(u);
+ if (onUpload) onUpload(u);
})
.catch(console.error);
}
From 9f9800d0f699a5afad6f636372348d7b976378ff Mon Sep 17 00:00:00 2001
From: Jack Hansen <47556286+jackavh@users.noreply.github.com>
Date: Sat, 8 Jun 2024 18:29:30 -0700
Subject: [PATCH 12/14] lint fix
---
frontend/src/components/NewsletterSidebar.tsx | 16 +---------------
1 file changed, 1 insertion(+), 15 deletions(-)
diff --git a/frontend/src/components/NewsletterSidebar.tsx b/frontend/src/components/NewsletterSidebar.tsx
index 115993e5..f2b64594 100644
--- a/frontend/src/components/NewsletterSidebar.tsx
+++ b/frontend/src/components/NewsletterSidebar.tsx
@@ -9,6 +9,7 @@ import styles from "./NewsletterSidebar.module.css";
import { TextField } from "./TextField";
import { WarningModule } from "./WarningModule";
import SimpleImageDropzone from "./admin/storage/SimpleImageDropzone";
+
import { deleteFile } from "@/app/admin/util/pageeditUtil";
type newsletterSidebarProps = {
@@ -54,21 +55,6 @@ const NewsletterSidebar = ({
setSidebarOpen(false);
};
- const handleCloseSidebar = () => {
- if (
- title !== (newsletter ? newsletter.title : "") ||
- description !== (newsletter ? newsletter.description : "") ||
- date !== (newsletter ? newsletter.date : "") ||
- image !== (newsletter ? newsletter.image : "") ||
- content !== (newsletter ? newsletter.content : [])
- ) {
- // not
- } else {
- confirmCancel();
- setSidebarOpen(false);
- }
- };
-
const handleSave = () => {
if (title === "" || description === "" || date === "" || image === "" || content.length === 0) {
setErrors({
From 6c39cf964435617f98cbf52f079a78a81ec38e6e Mon Sep 17 00:00:00 2001
From: jennymar
Date: Sat, 29 Jun 2024 16:54:21 -0700
Subject: [PATCH 13/14] newsletter / events sidebar integration with images
---
.../src/components/EventSidebar.module.css | 18 +--
frontend/src/components/EventSidebar.tsx | 138 +++++++++---------
.../components/NewsletterSidebar.module.css | 7 -
frontend/src/components/NewsletterSidebar.tsx | 19 ++-
frontend/src/components/TextArea.tsx | 33 +++++
frontend/src/components/TextAreaCharLimit.tsx | 9 +-
frontend/src/components/TextField.tsx | 11 --
.../src/components/TextFieldCharLimit.tsx | 3 +
8 files changed, 133 insertions(+), 105 deletions(-)
create mode 100644 frontend/src/components/TextArea.tsx
diff --git a/frontend/src/components/EventSidebar.module.css b/frontend/src/components/EventSidebar.module.css
index 8ecee7c3..01b39816 100644
--- a/frontend/src/components/EventSidebar.module.css
+++ b/frontend/src/components/EventSidebar.module.css
@@ -167,11 +167,11 @@
}
.textField {
- width: 454px;
+ width: 33vw;
}
.textFieldSmall {
- width: 240px;
+ width: 33vw;
}
.textFieldSmallest {
@@ -179,21 +179,11 @@
}
.textArea {
- width: 454px;
- height: 80px;
- padding: 11px;
- border: 1px solid #d8d8d8;
- border-radius: 4px;
- font-size: 14px;
+ width: 33vw;
}
.textAreaLong {
- width: 454px;
- height: 120px;
- padding: 10px;
- border: 1px solid #d8d8d8;
- border-radius: 4px;
- font-size: 14px;
+ width: 33vw;
}
.textAreaContent {
diff --git a/frontend/src/components/EventSidebar.tsx b/frontend/src/components/EventSidebar.tsx
index 08913760..d8336e5b 100644
--- a/frontend/src/components/EventSidebar.tsx
+++ b/frontend/src/components/EventSidebar.tsx
@@ -13,6 +13,7 @@ import { TextFieldCharLimit } from "./TextFieldCharLimit";
import AlertBanner from "@/components/AlertBanner";
import { TextField } from "@/components/TextField";
import { WarningModule } from "@/components/WarningModule";
+import { TextArea } from "./TextArea";
type eventSidebarProps = {
eventDetails: null | EventDetails;
@@ -247,24 +248,26 @@ const EventSidebar = ({
Image
Placeholder - to be replaced with image
- {/* Delete button */}
-
-
+
+
+
{
+ setIsDeleting(true);
+ }}
+ className={styles.deleteButton}
+ >
+ Delete
+
+
+
+
);
@@ -273,15 +276,21 @@ const EventSidebar = ({
if (isEditing) {
return (
-
{
- handleCloseSidebar();
+
{
+ void handleSave();
}}
>
-
- Close Window
-
+
+
Event Details
@@ -289,7 +298,7 @@ const EventSidebar = ({
@@ -389,8 +402,8 @@ const EventSidebar = ({
}}
error={errors.location}
/>
-
Guidelines
-
-
- {/* Cancel button */}
-
- Cancel
-
- {/* Save button */}
-
{
- void handleSave();
- }}
- className={styles.saveButton}
- >
- Save
-
-
- {warningOpen && (
-
-
-
- {
- void handleSave();
- }}
- onClose={() => {
- setWarningOpen(false);
- }}
- />
-
+
+
+
+ {/* Cancel button */}
+
+
+
+ {/* Save button */}
+
{
+ void handleSave();
+ }}
+ className={styles.saveButton}
+ >
+ Save
+
- )}
+
);
} else {
diff --git a/frontend/src/components/NewsletterSidebar.module.css b/frontend/src/components/NewsletterSidebar.module.css
index b78d3f9e..fcce8f7d 100644
--- a/frontend/src/components/NewsletterSidebar.module.css
+++ b/frontend/src/components/NewsletterSidebar.module.css
@@ -132,8 +132,6 @@
gap: 24px;
justify-content: flex-end;
width: 33vw;
- /* margin-right: 60px; */
- /* margin-bottom: 77px; */
}
.deleteButtonWrapper {
@@ -163,13 +161,8 @@
.textField {
width: 33vw;
}
-
.textArea {
width: 33vw;
- height: 288px;
- padding: 12px;
- border: 1px solid #d8d8d8;
- border-radius: 4px;
}
.content {
diff --git a/frontend/src/components/NewsletterSidebar.tsx b/frontend/src/components/NewsletterSidebar.tsx
index ae5ecbf5..3d9bd06d 100644
--- a/frontend/src/components/NewsletterSidebar.tsx
+++ b/frontend/src/components/NewsletterSidebar.tsx
@@ -11,6 +11,7 @@ import { WarningModule } from "./WarningModule";
import SimpleImageDropzone from "./admin/storage/SimpleImageDropzone";
import { deleteFile } from "@/app/admin/util/pageeditUtil";
+import { TextArea } from "./TextArea";
type newsletterSidebarProps = {
newsletter: null | Newsletter;
@@ -223,7 +224,7 @@ const NewsletterSidebar = ({
- {/*
Placeholder - to be replaced with image
*/}
-
Newsletter Content
-
@@ -296,7 +298,7 @@ const NewsletterSidebar = ({
{/* Cancel button */}
{/* Save button */}
-
+ {
+ void handleSave();
+ }}
+ className={styles.saveButton}
+ >
Save
diff --git a/frontend/src/components/TextArea.tsx b/frontend/src/components/TextArea.tsx
new file mode 100644
index 00000000..b019f797
--- /dev/null
+++ b/frontend/src/components/TextArea.tsx
@@ -0,0 +1,33 @@
+import React from "react";
+
+import styles from "./TextField.module.css";
+
+export type TextAreaProps = {
+ label: string;
+ error?: boolean;
+} & Omit, "type">;
+
+export const TextArea = function ({
+ label,
+ error = false,
+ className,
+ placeholder,
+ ...props
+}: TextAreaProps) {
+ let wrapperClass = styles.wrapper;
+ if (className) {
+ wrapperClass += ` ${className}`;
+ }
+ let inputClass = styles.input;
+ if (error) {
+ inputClass += ` ${styles.error}`;
+ }
+ return (
+
+ );
+};
diff --git a/frontend/src/components/TextAreaCharLimit.tsx b/frontend/src/components/TextAreaCharLimit.tsx
index 5a84e520..a341f692 100644
--- a/frontend/src/components/TextAreaCharLimit.tsx
+++ b/frontend/src/components/TextAreaCharLimit.tsx
@@ -1,32 +1,39 @@
import React from "react";
import { CharacterCount } from "@/components/CharacterCount";
+import { TextArea } from "./TextArea";
export type TextAreaCharLimitProps = {
+ label: string;
id: string;
className: string;
placeholder: string;
value: string;
maxCount: number;
onChange: (event: React.ChangeEvent) => void;
+ error?: boolean;
} & Omit, "type">;
export const TextAreaCharLimit = ({
+ label,
id,
className,
placeholder,
value,
maxCount,
onChange,
+ error,
}: TextAreaCharLimitProps) => {
return (
-
{value.length > maxCount ? (
diff --git a/frontend/src/components/TextField.tsx b/frontend/src/components/TextField.tsx
index c097e462..c8a24e55 100644
--- a/frontend/src/components/TextField.tsx
+++ b/frontend/src/components/TextField.tsx
@@ -2,22 +2,11 @@ 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, placeholder, ...props },
ref,
diff --git a/frontend/src/components/TextFieldCharLimit.tsx b/frontend/src/components/TextFieldCharLimit.tsx
index 553a5bce..981dcb0a 100644
--- a/frontend/src/components/TextFieldCharLimit.tsx
+++ b/frontend/src/components/TextFieldCharLimit.tsx
@@ -5,6 +5,7 @@ import { TextField } from "@/components/TextField";
export type TextFieldCharLimitProps = {
label: string;
+ className: string;
placeholder: string;
value: string;
maxCount: number;
@@ -14,6 +15,7 @@ export type TextFieldCharLimitProps = {
export const TextFieldCharLimit = ({
label,
+ className,
placeholder,
value,
maxCount,
@@ -24,6 +26,7 @@ export const TextFieldCharLimit = ({
Date: Sat, 29 Jun 2024 16:59:25 -0700
Subject: [PATCH 14/14] linting
---
frontend/src/components/EventSidebar.tsx | 63 +------------------
frontend/src/components/NewsletterSidebar.tsx | 10 +--
frontend/src/components/TextAreaCharLimit.tsx | 3 +-
frontend/src/components/WarningModule.tsx | 2 +-
.../admin/storage/SimpleImageDropzone.tsx | 4 +-
5 files changed, 12 insertions(+), 70 deletions(-)
diff --git a/frontend/src/components/EventSidebar.tsx b/frontend/src/components/EventSidebar.tsx
index d8336e5b..701698b8 100644
--- a/frontend/src/components/EventSidebar.tsx
+++ b/frontend/src/components/EventSidebar.tsx
@@ -4,16 +4,16 @@ import React, { useState } from "react";
import DatePicker from "react-datepicker";
import "react-datepicker/dist/react-datepicker.css";
-import { CreateEventDetailsRequest, EventDetails, deleteEventDetails } from "../api/eventDetails";
+import { CreateEventDetailsRequest, EventDetails } from "../api/eventDetails";
import styles from "./EventSidebar.module.css";
+import { TextArea } from "./TextArea";
import { TextAreaCharLimit } from "./TextAreaCharLimit";
import { TextFieldCharLimit } from "./TextFieldCharLimit";
import AlertBanner from "@/components/AlertBanner";
import { TextField } from "@/components/TextField";
import { WarningModule } from "@/components/WarningModule";
-import { TextArea } from "./TextArea";
type eventSidebarProps = {
eventDetails: null | EventDetails;
@@ -53,7 +53,6 @@ const EventSidebar = ({
const [isEditing, setIsEditing] = useState(!eventDetails);
const [isDeleting, setIsDeleting] = useState(false);
const [errors, setErrors] = useState({});
- const [warningOpen, setWarningOpen] = useState(false);
const [showAlert, setShowAlert] = useState(false);
const confirmCancel = () => {
@@ -68,51 +67,10 @@ const EventSidebar = ({
setIsEditing(false);
setIsDeleting(false);
setErrors({});
- setWarningOpen(false);
setSidebarOpen(false);
};
- const handleCancel = () => {
- const defaultDate = new Date();
- if (
- name !== (eventDetails ? eventDetails.name : "") ||
- description !== (eventDetails ? eventDetails.description : "") ||
- description_short !== (eventDetails ? eventDetails.description_short : "") ||
- date !== (eventDetails ? new Date(eventDetails.date) : defaultDate) ||
- startTime !== (eventDetails ? eventDetails.startTime : "") ||
- endTime !== (eventDetails ? eventDetails.endTime : "") ||
- location !== (eventDetails ? eventDetails.location : "") ||
- guidelines !== (eventDetails ? eventDetails.guidelines : "")
- ) {
- setWarningOpen(true);
- } else {
- confirmCancel();
- }
- };
-
- const handleCloseSidebar = () => {
- const defaultDate = new Date();
- if (
- name !== (eventDetails ? eventDetails.name : "") ||
- description !== (eventDetails ? eventDetails.description : "") ||
- description_short !== (eventDetails ? eventDetails.description_short : "") ||
- date !== (eventDetails ? new Date(eventDetails.date) : defaultDate) ||
- startTime !== (eventDetails ? eventDetails.startTime : "") ||
- endTime !== (eventDetails ? eventDetails.endTime : "") ||
- location !== (eventDetails ? eventDetails.location : "") ||
- guidelines !== (eventDetails ? eventDetails.guidelines : "")
- ) {
- setWarningOpen(true);
- } else {
- confirmCancel();
- setSidebarOpen(false);
- }
- };
-
const handleSave = async () => {
- setWarningOpen(false);
- console.log("handleSave");
-
if (
name === "" ||
description === "" ||
@@ -178,23 +136,6 @@ const EventSidebar = ({
setIsDeleting(true);
};
- const confirmDelete = () => {
- if (eventDetails) {
- deleteEventDetails(eventDetails._id)
- .then((result) => {
- if (result.success) {
- window.location.reload();
- } else {
- console.error("ERROR:", result.error);
- }
- })
- .catch((error) => {
- alert(error);
- });
- setSidebarOpen(false);
- }
- };
-
const alertContent = {
text: "Event Saved!",
};
diff --git a/frontend/src/components/NewsletterSidebar.tsx b/frontend/src/components/NewsletterSidebar.tsx
index 3d9bd06d..24fd2662 100644
--- a/frontend/src/components/NewsletterSidebar.tsx
+++ b/frontend/src/components/NewsletterSidebar.tsx
@@ -6,12 +6,12 @@ import { CreateNewsletterRequest, Newsletter, deleteNewsletter } from "../api/ne
import AlertBanner from "./AlertBanner";
import styles from "./NewsletterSidebar.module.css";
+import { TextArea } from "./TextArea";
import { TextField } from "./TextField";
import { WarningModule } from "./WarningModule";
import SimpleImageDropzone from "./admin/storage/SimpleImageDropzone";
import { deleteFile } from "@/app/admin/util/pageeditUtil";
-import { TextArea } from "./TextArea";
type newsletterSidebarProps = {
newsletter: null | Newsletter;
@@ -93,11 +93,11 @@ const NewsletterSidebar = ({
};
// handle changing url on newsletter to "" if user deletes image
- const onImageDelete = () => {
+ const onImageDelete = async () => {
setImage("");
// immediately update newsletter, can't undo image delete
if (newsletter) {
- updateNewsletter({
+ await updateNewsletter({
...newsletter,
image: "",
});
@@ -105,10 +105,10 @@ const NewsletterSidebar = ({
};
// handle updating image on image dropzone upload
- const onImageUpload = (url: string) => {
+ const onImageUpload = async (url: string) => {
// can't undo image upload, save immediately
if (newsletter) {
- updateNewsletter({
+ await updateNewsletter({
...newsletter,
image: url,
});
diff --git a/frontend/src/components/TextAreaCharLimit.tsx b/frontend/src/components/TextAreaCharLimit.tsx
index a341f692..ae8e86ed 100644
--- a/frontend/src/components/TextAreaCharLimit.tsx
+++ b/frontend/src/components/TextAreaCharLimit.tsx
@@ -1,8 +1,9 @@
import React from "react";
-import { CharacterCount } from "@/components/CharacterCount";
import { TextArea } from "./TextArea";
+import { CharacterCount } from "@/components/CharacterCount";
+
export type TextAreaCharLimitProps = {
label: string;
id: string;
diff --git a/frontend/src/components/WarningModule.tsx b/frontend/src/components/WarningModule.tsx
index 900ed77b..3ad1fa03 100644
--- a/frontend/src/components/WarningModule.tsx
+++ b/frontend/src/components/WarningModule.tsx
@@ -12,7 +12,7 @@ type WarningModuleProps = {
cancelText: string;
actionText: string;
cancel?: () => void;
- action: () => void;
+ action: () => unknown;
children: ReactNode;
};
diff --git a/frontend/src/components/admin/storage/SimpleImageDropzone.tsx b/frontend/src/components/admin/storage/SimpleImageDropzone.tsx
index 779b7ca3..76b84382 100644
--- a/frontend/src/components/admin/storage/SimpleImageDropzone.tsx
+++ b/frontend/src/components/admin/storage/SimpleImageDropzone.tsx
@@ -11,8 +11,8 @@ type ImageDropProps = {
folder: string;
url: string;
setUrl: (url: string) => void;
- onDelete?: () => void;
- onUpload?: (url: string) => void;
+ onDelete?: () => unknown;
+ onUpload?: (url: string) => unknown;
};
export default function SimpleImageDropzone({