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) => ( +
+
+ {`Image +
+
+ ))} +
+ +
+
+ ); + } \ 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 (
); })} + {imageUploadBox && +
+

Image Upload

+ +
}
)}
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() {
{`Image ))}
- +
); 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 ( +
+ +
+ ); +}; + +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 && 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 ( +
+ +
+ ); +}; + +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 ( + <> +
+
+ Image gallery slot +
+
+
+ + + +
+ + ); +}; + +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 ( +
+

{field.name}

+ +
+ ); +}; 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 ( +
+

{field.name}

+ +
+ ); +}; 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.

+
+
+ + +
+
+
+
+
+ +
+ ); +} 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 ( +
+ +

+ {message} +

+
+ ); +} + +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}

-
- Story image 1 -
- -
- Story image 2 -
-
-
- Story image 3 +
+
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) => ( +
+ sponsor +
+ ))} +
- - Sponsors
-
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 (
-
- {showAlert && } -
-
- - - - -
- -
-
+ + +
); } 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 (
-
- {showAlert && } -
-
- {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 (
-
- {showAlert && } -
-
- {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) {
- Description of the image
Here’s Our Story @@ -90,10 +92,12 @@ export default function NewsletterDisplay({ params }: Props) {
Share This Post - facebook Icon - twitter Icon
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) => (
- sponsor + sponsor
))}
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: () => ( -
-
Email
- -
- ), - }, - ]; - 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: () => ( +
+
Email
+ +
+ ), + }, + ]; + return (
@@ -350,9 +350,11 @@ export default function MailingList() { lineHeight: "24px", }} // Make room for the image /> - search icon - Add Icon + Add Icon 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 && -
-

Image Upload

- -
} + {hasGalleryBox && ( +
+

Image Upload

+ +
+ )}
)}
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 (
- Contact Icon + Contact Icon
{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 (
- {/*
*/}
- Default Logo + Default Logo
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) => ( -
-
- {`Image -
-
- ))} -
- -
-
- ); - } \ 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 (
- Image 1 + Value Icon
{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} + > +
+ +
+
+
); @@ -211,20 +195,6 @@ const NewsletterSidebar = ({ if (isEditing) { return (
- {warningOpen &&
} - {warningOpen && ( - { - setWarningOpen(false); - }} - /> - )}
{ @@ -282,15 +252,28 @@ const NewsletterSidebar = ({
-
- {/* Cancel button */} - - {/* Save button */} - +
+
+ {/* Cancel button */} + +
+

Cancel

+
+
+ {/* Save button */} + +
); @@ -339,11 +322,25 @@ const NewsletterSidebar = ({

Newsletter Content

{content}
{/* Delete button */} -
- -
+ +
+ +
+
); 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 = ({
- +
); }; 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) {
Description of the image { 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); }} > - test -

Close Window

-
+
+ test +

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